@lovek4d/kickflip 0.6.0 → 0.7.0
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 +42 -26
- package/SKILL.md +16 -12
- package/bin/doctor.mjs +5 -4
- package/bin/event-lines.mjs +1 -0
- package/bin/install.mjs +3 -3
- package/bin/kickflip.mjs +7 -7
- package/bin/wait-events.mjs +2 -2
- package/board/agents.mjs +2 -2
- package/board/arch-review.mjs +1 -1
- package/board/bin/board.mjs +70 -42
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +8 -9
- package/board/onboarding.mjs +1 -1
- package/board/prototype-page.mjs +1 -1
- package/board/public/app-arch.js +2 -2
- package/board/public/app-banner.js +1 -1
- package/board/public/app-cards.js +36 -48
- package/board/public/app-lanes.js +6 -9
- package/board/public/app-needs.js +107 -26
- package/board/public/app-reader.js +52 -42
- package/board/public/app-threads.js +14 -10
- package/board/public/app.html +1 -1
- package/board/public/app.js +22 -12
- package/board/public/progress-review.js +4 -4
- package/board/public/prototype-kit.js +25 -11
- package/board/public/prototype-switcher.js +53 -33
- package/board/public/prototype.html +7 -4
- package/board/public/router.js +34 -6
- package/board/public/shell.js +23 -16
- package/board/public/sidebar.css +7 -2
- package/board/public/sidebar.js +3 -0
- package/board/public/style.css +69 -30
- package/board/public/ui-cards.js +61 -38
- package/board/public/ui-enums.js +12 -3
- package/board/public/ui-header.js +4 -6
- package/board/public/ui-hub.js +7 -4
- package/board/public/ui-links.js +1 -1
- package/board/public/ui-order.js +9 -6
- package/board/public/ui-page-header.js +1 -1
- package/board/public/ui-progress-bar.js +6 -7
- package/board/public/ui-questions.js +18 -3
- package/board/public/ui-sidebar.js +12 -5
- package/board/public/ui-snapshot.js +1 -1
- package/board/public/ui-threads.js +10 -12
- package/board/public/ui.js +7 -4
- package/board/public/view-hub.js +3 -1
- package/board/public/view-notes.js +1 -1
- package/board/public/view-questions.js +19 -3
- package/board/public/view-settings.js +60 -28
- package/board/server.mjs +19 -19
- package/board/stop.mjs +1 -1
- package/board/store.mjs +27 -25
- package/board/tickets.mjs +1 -1
- package/board/transcript.mjs +4 -2
- package/hooks-plugin/hooks/board-data-gate.mjs +9 -6
- package/hub/front-door.mjs +1 -1
- package/hub/home.mjs +1 -1
- package/hub/hub.mjs +6 -6
- package/hub/sessions.mjs +2 -2
- package/hub/try.mjs +7 -7
- package/hub/upgrade.mjs +1 -1
- package/hub/usage.mjs +3 -3
- package/package.json +1 -1
- package/shared/card-prs.mjs +1 -1
- package/shared/machine-gate.mjs +2 -2
- package/shared/tailscale.mjs +1 -1
- package/shared/usage-gate.mjs +1 -1
package/board/store.mjs
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
|
-
var
|
|
2
|
-
`).filter(Boolean).map(t=>JSON.parse(t))}catch{return[]}},"readLines");async function
|
|
3
|
-
`):
|
|
4
|
-
`)})},"writeJSON");async function
|
|
5
|
-
`)}r(
|
|
6
|
-
`).filter(k=>k.trim())}))}),
|
|
7
|
-
`)),{exitCode:
|
|
8
|
-
`).map(n=>n.replace(/\s+/g," ").trim()).find(Boolean)||"";return t.length>
|
|
1
|
+
var Xi=Object.defineProperty;var r=(e,t)=>Xi(e,"name",{value:t,configurable:!0});import P from"node:fs/promises";import{constants as eo,existsSync as fe,readFileSync as to}from"node:fs";import m from"node:path";import ln from"node:os";import{execFile as ca}from"node:child_process";import{fileURLToPath as no}from"node:url";import{BUILD_REFUSED as da,BUILDER_ASK as It,COMMANDS as he,LANDER as la,LANES as Ke,MOVE as ua,PHASES as un,handedBackAt as fn,MAX_IMPLEMENTERS as ao,MAX_PLANNERS as io,builds as Je,lineupSteps as wn,mergeBlockers as oo,modelName as Bt,noTakePicked as so,ownerQuestion as Ie,plans as Be,QUESTION_REOPEN as ro,PLANNER_WORD as co,providerOf as ft,owesNext as pn,pushTag as lo,refOf as Qe,roundOf as hn,roundSettledBy as uo,HELPER as se,VISUAL as we,visualAgent as fo}from"./domain.mjs";import{GITHUB_STATE as mn,appendEvent as me,listBoards as ge,samePath as je}from"../hub/boards.mjs";import{readUsageGate as fa,pauseReason as wo,pauseReasonFor as po,heldFor as ho,holdsWork as mo,builderPick as gn,pauseEnd as go}from"../shared/usage-gate.mjs";import{reviseQueued as $o,pushKindError as ko,readClaudeModel as $n}from"../hub/push.mjs";import{AT_RE as bo}from"../hub/duration.mjs";import{routeRecord as X}from"../hub/routes.mjs";import{ticketSource as kn,ticketIn as bn,resolveTicket as yo,ticketTarget as xo,moveTicket as vo,titleTickets as _o,cardIdOf as So}from"./tickets.mjs";import{LINEAR_NEEDS_KEY as jo,linearStatus as Oo,linearWorks as Po}from"../hub/linear.mjs";import{validateBranch as Do}from"../hub/repos.mjs";import{withFileLock as wt}from"../hub/locks.mjs";import{audit as Ao,auditLines as Ro,boardAudit as wa,readAudit as No}from"../shared/audit.mjs";import{merge3 as Eo}from"./merge3.mjs";import{fetchPR as pa,reportOf as ha,githubLogin as Io,postToPR as Bo,postReview as Lo,reviewsSince as Co,CLAUDE_PREFIX as pt,CLAUDE_MARK as yn}from"../hub/pr-report.mjs";import{allLanded as To,cardPRs as Le,hasPR as Ce,parsePRRef as xn,prKeyOf as ht,prNames as Ge,prURL as vn,storedPRs as Oe}from"../shared/card-prs.mjs";import{reviewLinks as qo,topicItems as Fo,reviewersOf as _n}from"./review.mjs";import{readiness as ma}from"../hub/readiness.mjs";import{slackDay as Mo}from"../hub/slack.mjs";import"../bin/install.mjs";import{readPermissions as Wo}from"./permissions.mjs";import{validBuildTarget as ga}from"./build-script.mjs";import{parseCheck as Uo,checkText as Ho}from"../shared/owner-check.mjs";import{RuleError as zo,rulesOf as $a,addRule as Ko,editRule as Jo,removeRule as Qo,applyRules as Go,readHubRules as Lt,writeHubRules as Yo}from"../hub/rules.mjs";import{shareEntries as Zo,validLogin as Vo}from"../hub/share.mjs";class re extends Error{static{r(this,"BoardError")}}const l=r(e=>{throw new re(e)},"fail"),Xo=r(()=>m.join(ln.homedir(),".claude","kickflip","sites"),"sitesDir"),Ct=r(()=>m.join(ln.homedir(),".claude","waves"),"wavesDir"),ka=Symbol("read as"),$=r(async(e,t)=>{let n;try{n=await P.readFile(e,"utf8")}catch{return t}try{const a=JSON.parse(n);return a!==null&&typeof a=="object"&&!Array.isArray(a)?{...a,[ka]:n}:a}catch{return t}},"readJSON"),G=r(async e=>{try{return(await P.readFile(e,"utf8")).split(`
|
|
2
|
+
`).filter(Boolean).map(t=>JSON.parse(t))}catch{return[]}},"readLines");async function mt(e,t){await P.mkdir(m.dirname(e),{recursive:!0});const n=`${e}.${process.pid}.tmp`;await P.writeFile(n,t),await P.rename(n,e)}r(mt,"replace");const y=r((e,t)=>{const{[ka]:n,...a}=t??{};return n===void 0?mt(e,JSON.stringify(t,null,2)+`
|
|
3
|
+
`):wt(e,async()=>{const i=JSON.parse(n),o=JSON.parse(await P.readFile(e,"utf8").catch(()=>n));await mt(e,JSON.stringify(Eo(i,a,o),null,2)+`
|
|
4
|
+
`)})},"writeJSON");async function z(e,t){await P.mkdir(m.dirname(e),{recursive:!0}),await P.appendFile(e,JSON.stringify(t)+`
|
|
5
|
+
`)}r(z,"appendLine");async function rc(e){/^[\w-]+$/.test(e||"")||l(`--site takes a route such as ios-orchestrator, not "${e}"`);const t=m.join(Xo(),e);return await P.stat(t).then(n=>n.isDirectory(),()=>!1)||l(`no site at ${t}`),t}r(rc,"siteDir");const ie=r(async e=>({title:"Board",items:"waves",...e?await $(m.join(e,"board.json"),{}):{}}),"boardConfig");async function F(e){const t=await ie(e);if(t.items==="waves")try{return(await P.readdir(Ct(),{withFileTypes:!0})).filter(a=>a.isDirectory()).map(a=>({slug:a.name,dir:m.join(Ct(),a.name),file:m.join(Ct(),a.name,"wave.json")}))}catch{return[]}const n=m.resolve(e,t.items);return[{slug:t.slug||m.basename(t.items,".json"),dir:m.dirname(n),file:n}]}r(F,"itemSources");const M=r((e,t)=>m.join(e,"data","threads",t.replace(/[^\w.-]+/g,"__")+".jsonl"),"threadFile"),L=r(e=>m.join(e,"data","owner.json"),"ownerFile"),$e=r(e=>m.join(e,"data","staged-questions.json"),"stagedFile"),Ye=r(e=>m.join(e,"data","screening.json"),"screeningFile"),es=900*1e3,A=r(e=>e||l("this command needs --site <route>"),"needSite"),S=r((e,t)=>typeof e=="string"&&e.trim()?e.trim():l(`${t} must be non-empty text`),"text"),ne=r((e,t)=>e===void 0?void 0:t(e),"optional"),ts=/^prototype-[\w-]+\.html(?:[?#]\S*)?$/,ns=/^visuals\/[\w-]+\/[\w-]+\.html$/,ba=r((e,t)=>typeof e=="string"&&(ts.test(e)||ns.test(e)||/^https?:\/\/\S+$/.test(e))?e:l(`${t} must be an http(s) URL or a site page such as prototype-<topic>.html?q=<id>`),"pageUrl"),Tt=r((e,t)=>typeof e=="boolean"?e:l(`${t} must be true or false`),"bool"),Pe=r((e,t)=>typeof e=="string"&&/^[\w.-]+$/.test(e)?e:l(`${t} must use only letters, digits, "_", "." and "-"`),"domId"),Ze=r((e,t,n)=>{(!e||typeof e!="object"||Array.isArray(e))&&l(`${n} must be a JSON object`);const a=Object.keys(e).filter(i=>!t.includes(i));a.length&&l(`${n} has unknown field ${a.map(i=>`"${i}"`).join(", ")}; allowed: ${t.join(", ")}`)},"onlyKeys");function I(e){const t=/^([^/\s]+)\/([^/\s]+)$/.exec(e||"");return t?{wave:t[1],id:t[2]}:l(`expected <wave>/<id>, not "${e}"`)}r(I,"parseKey");async function Te(e,t){const n=(await F(e)).find(i=>i.slug===t)||l(`this board has no wave "${t}"`),a=await $(n.file,null);return(!a||!Array.isArray(a.items))&&l(`${n.file} is not a wave file with items[]`),{source:n,wave:a}}r(Te,"loadWave");async function x(e,t){const{wave:n,id:a}=I(t);let i=await Te(e,n);!i.wave.items.some(s=>s.id===a)&&await fr(e,t)&&(i=await Te(e,n));const o=i.wave.items.find(s=>s.id===a)||l(`wave "${n}" has no item "${a}"`);return{...i,item:o}}r(x,"loadItem");async function cc(e,t,n,a){A(e),Pe(t,"the event id");const i=(await G(m.join(e,"data","events.jsonl"))).find(w=>w.id===t);i||l(`no event "${t}" in this site's data/events.jsonl`);const o=i.kind==="ask"?`ask:${t}`:i.kind==="item-message"&&typeof i.data?.key=="string"?i.data.key:null,s=a===void 0?void 0:/^ask:(.+)$/.exec(o||"")?.[1]||l(`reply --title names an ask: "${t}" is not an ask or a message in an ask's thread`);s&&S(a,"--title");const c=m.join(e,"data","replies",`${t}.html`);await mt(c,S(n,"the reply")),s&&await Ea(e,s,a);const d=o?await va(e,o,fs(n)):[],u=i.kind==="wave-triage"?["lineup","wave"].filter(w=>typeof i.data?.[w]=="string").map(w=>`${w}:${i.data[w]}`):[],p=i.kind==="ask"?`item:ask:${t}`:u[0];return p&&d.push(...await ii(e,p,"Acted on: replied")),{file:c,lines:d}}r(cc,"writeReply");const ya=r(e=>m.join(e,"data","working.json"),"workingFile");async function as(e){return(await $(ya(e),{})).events||{}}r(as,"readWorking");const dc=["ask","item-message","wave-triage","overnight-triage"];async function lc(e,t){if(!t.length)return;const n=new Date().toISOString(),a=await as(e);await y(ya(e),{events:{...a,...Object.fromEntries(t.map(i=>[i,X(a,i)||n]))}})}r(lc,"markWorking");const Sn=r(e=>m.join(e,"data","push-state.json"),"pushStateFile");async function is(e,t){A(e);const n=Date.parse((await $(Sn(e),{})).last_push);if(Number.isNaN(n))return 0;const a=m.join(e,"data","replies"),i=(await P.readdir(a).catch(()=>[])).filter(s=>/\.(html|json)$/.test(s));return(await Promise.all(i.map(s=>P.stat(m.join(a,s)).then(c=>c.mtimeMs,()=>0)))).filter(s=>s>n&&s<=t.getTime()).length}r(is,"repliesSincePush");async function os(e,t){return await y(Sn(A(e)),{last_push:t.toISOString()}),Sn(e)}r(os,"recordPush");const ss=m.join(m.dirname(no(import.meta.url)),"..","bin","kickflip.mjs");function rs(){if(!process.env.KICKFLIP_PUSH_CMD)return[process.execPath,ss,"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 re("KICKFLIP_PUSH_CMD must be a JSON array of strings")}r(rs,"pushCommand");async function uc(e,t){const n=ko(t.kind);n&&l(n),t.title===void 0&&l("push needs --title");const a=new Date,i=ds(t.body,await is(e,a)),o={kind:t.kind,title:t.title,body:i||void 0,url:ws(t.url),tag:t.tag},[s,...c]=rs(),d=[...c,...Object.entries(o).flatMap(([w,f])=>f===void 0?[]:[`--${w}`,f]),...t.urgent?["--urgent"]:[],...t.now?["--now"]:[]],u=await new Promise(w=>{ca(s,d,{encoding:"utf8"},(f,h,g)=>w({error:f,said:`${h||""}${g||""}`.split(`
|
|
6
|
+
`).filter(k=>k.trim())}))}),p=u.error?typeof u.error.code=="number"?u.error.code:1:0;if(p!==0)throw Object.assign(new re([...u.said,`kickflip push failed (${typeof u.error.code=="number"?`exit ${p}`:u.error.message}); the push was not recorded`].join(`
|
|
7
|
+
`)),{exitCode:p});return await os(e,a),{lines:u.said}}r(uc,"sendPush");const cs=/(?:^| · )(and \d+ repl(?:y|ies))$/,ds=r((e,t)=>[e,t&&`and ${t} ${t===1?"reply":"replies"}`].filter(Boolean).join(" \xB7 "),"withReplies"),xa=160;function ls(e){const t=String(e).split(`
|
|
8
|
+
`).map(n=>n.replace(/\s+/g," ").trim()).find(Boolean)||"";return t.length>xa?`${t.slice(0,xa-1).trimEnd()}\u2026`:t}r(ls,"pushLine");const us={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" "};function fs(e){return e.replace(/<details[\s\S]*?<\/details>/gi,`
|
|
9
9
|
`).replace(/<(?:br|\/?(?:p|div|li|ul|ol|h\d|blockquote|pre|tr|summary))\b[^>]*>/gi,`
|
|
10
|
-
`).replace(/<[^>]*>/g,"").replace(/&(?:#(x[\da-f]{1,6}|\d{1,7})|(\w+));/gi,(t,n,a)=>{const i=n&&parseInt(n.replace(/^x/i,""),/^x/i.test(n)?16:10);return n?i<=1114111?String.fromCodePoint(i):t:Z(qo,a.toLowerCase())??t})}r(Fo,"htmlText");function Mo(e){if(!e||!e.startsWith("/"))return e;const t=new URL(e,"http://board");return/^#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(Mo,"pushUrl");const Wo=r((e,t)=>`/${$.basename(e)}/#item=${encodeURIComponent(t)}&reply`,"threadUrl");async function da(e,t,n){const a=To(n);if(!a)return[];const i=Ti(t),o=await Ki(i,s=>[a,Bo.exec(s)?.[1]].filter(Boolean).join(" \xB7 "),Wo(e,t));return o===null?[]:[`queued push ${i} now reads: ${o}`]}r(da,"freshenPush");const ua=r(e=>$.join(e,"data","hidden.json"),"hiddenFile"),Uo=r(async e=>new Set((await g(ua(e),{events:[]})).events||[]),"readHidden");let fa=Promise.resolve();function Ir(e,t,n){ge(t,"the event id");const a=fa.then(async()=>{const i=await Uo(e);n?i.add(t):i.delete(t),await y(ua(e),{events:[...i]})});return fa=a.catch(()=>{}),a}r(Ir,"setHidden");const pa=r(e=>$.join(e,"data","read.json"),"readMarksFile"),Ho=r(async e=>(await g(pa(e),{})).cards||{},"readMarks");let wa=Promise.resolve();function Br(e,t,n={}){const a=wa.then(async()=>{const i=await Ho(e),o=new Date().toISOString(),s=r((l,u)=>String(u||"")>l?u:l,"later"),c=Object.fromEntries([...new Set(t)].sort().map(l=>[l,s(i[l]||o,n[l])]));JSON.stringify(c)!==JSON.stringify(i)&&await y(pa(e),{cards:c})});return wa=a.catch(()=>{}),a}r(Br,"markRead");const mn=20*1024*1024,De=r(e=>$.join(e,"data","uploads"),"uploadsDir"),We=r((e,t)=>$.join(De(e),".meta",`${t}.json`),"uploadMeta"),ma=/^\d+-[\w.-]+$/,ha=/^[\w.+-]+\/[\w.+-]+$/,ga=r(()=>Object.assign(new ie(`a file can be at most ${mn/1024/1024} MB`),{status:413}),"tooLarge");async function Cr(e,t,{name:n,type:a,length:i}){if(P(e),i>mn)throw ga();const o=String(n||"").trim().slice(0,200)||"file",s=$.basename(o).replace(/[^\w.-]+/g,"-").replace(/^[.-]+/,"").slice(-80)||"file",c=String(a||"").split(";")[0].trim().toLowerCase(),l=ha.test(c)?c:"application/octet-stream";await A.mkdir($.dirname(We(e,"x")),{recursive:!0});let u,w;for(let m=Date.now();!w;m++)u=`${m}-${s}`,w=await A.open($.join(De(e),u),"wx").catch(h=>h.code==="EEXIST"?null:Promise.reject(h));const p=$.join(De(e),u);let f=0;try{for await(const m of t){if(f+=m.length,f>mn)throw ga();await w.write(m)}}catch(m){throw await w.close(),await A.rm(p,{force:!0}),m}return await w.close(),await y(We(e,u),{name:o,type:l,size:f,at:new Date().toISOString()}),{name:o,url:`uploads/${u}`,path:p,type:l,size:f}}r(Cr,"saveUpload");async function Tr(e,t){if(!ma.test(t||""))return null;const n=$.join(De(e),t),a=await A.stat(n).catch(()=>null);if(!a?.isFile())return null;const i=await g(We(e,t),{});return{path:n,name:i.name||t,type:ha.test(i.type||"")?i.type:"application/octet-stream",size:a.size}}r(Tr,"readUpload");const $a=["github","slack"];async function J(e,t,{text:n,link:a,log:i,who:o="claude",via:s,kind:c,by:l,command:u,card:w}){P(e),String(t).startsWith("ask:")?await lt(e,t.slice(4),"the ask"):await v(e,t),/^[^\n]{1,40}$/.test(o.trim())||d("--who takes claude, love or a reviewer's name"),s!==void 0&&!$a.includes(s)&&d(`--via takes ${$a.join(" or ")}`);const p={ts:new Date().toISOString(),who:o.trim(),text:j(n,"--text"),link:Y(a,m=>ra(m,"--link")),log:i,via:s,kind:c,by:l,command:u,card:w},f=U(e,t);return await V(f,p),{file:f,lines:p.who==="claude"&&!s?await da(e,t,p.text):[]}}r(J,"appendThread");const ve=r(e=>$.join(e,"data","ask-links.json"),"askLinksFile");async function Re(e){return(await g(ve(e),{})).links||{}}r(Re,"readAskLinks");async function X(e,t){const{tickets:n}=_e(await te(e)),a=await T(e),i=/^ask:(.+)$/.exec(t)?.[1];if(i===void 0){const s=/^([^/\s]+)\/([^/\s]+)$/.exec(t);if(!s)return t;const c=a.find(w=>w.slug===s[1]),l=c&&await g(c.file,null);if(!l)return s[2];const u=(l.items||[]).find(w=>w.id===s[2]);return St(l,u||{id:s[2]},n)}const o=e?Z(await Re(e),i):void 0;if(o)return X(e,o);for(const s of a){const c=await g(s.file,null),l=(Array.isArray(c?.items)?c.items:[]).find(u=>u.from_ask===i);if(l)return St(c,l,n)}return`task ${i}`}r(X,"cardName");async function qr(e,t){const n=(await K($.join(e,"data","events.jsonl"))).find(a=>a.id===t);return n?.kind==="ask"?X(e,`ask:${t}`):typeof n?.data?.key=="string"?X(e,n.data.key):`the ${n?.kind} event`}r(qr,"eventCard");async function Fr(e,t,n){P(e),ge(t,"the event id");const a=j(n,"--text"),i=(await K($.join(e,"data","events.jsonl"))).find(s=>s.id===t)||d(`no event "${t}" in this site's data/events.jsonl`),o=Ri(i)||d(`event "${t}" isn't intake: a new task, the owner's message in an ask's thread, or a card an agent added`);return await re({dir:e},"intake-handoff",{event:t,key:o,text:a}),await J(e,o,{text:`Intake triage handed this to the orchestrator: ${a}`,kind:"status"}),{lines:[]}}r(Fr,"intakeHandoff");async function Mr(e,t,n,a){P(e),ge(t,"the event id"),(await K($.join(e,"data","events.jsonl"))).some(c=>c.id===t&&c.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`),await hn(e,t),await v(e,n);const o={ts:new Date().toISOString(),who:"claude",text:j(a,"--summary")},s=await Re(e);return await y(ve(e),{links:{...s,[t]:n}}),await V(U(e,n),o),ve(e)}r(Mr,"relateAsk");async function Wr(e,t){await lt(e,t,"the ask");const n=await Re(e);return Z(n,t)===void 0&&d(`ask "${t}" is not folded into a card`),delete n[t],await y(ve(e),{links:n}),ve(e)}r(Wr,"unfoldAsk");const Rt=r(e=>$.join(e,"data","ask-progress.json"),"askProgressFile");async function Nt(e){return(await g(Rt(e),{})).asks||{}}r(Nt,"readAskProgress");async function hn(e,t){const n=Z(await Nt(e),t)?.moved;n&&d(`ask "${t}" was moved to /${n.route}; it lives there now`)}r(hn,"refuseMoved");async function Ur(e,t,n,a){await lt(e,t,"the ask"),await hn(e,t);const i=await Nt(e);return await y(Rt(e),{asks:{...i,[t]:{state:n,at:new Date().toISOString(),...a===void 0?{}:{next:j(a,"--next")}}}}),Rt(e)}r(Ur,"markAsk");async function zo(e,t,n){const a=`ask "${t}"`;if(n.some(w=>w.kind==="ask-withdrawn"&&w.data?.id===t&&!w.data?.key))return{why:`${a} was deleted by the owner`};const i=Z(await Nt(e),t);if(i?.moved)return{why:`${a} was already moved to /${i.moved.route}`};const o=Z(await Re(e),t);if(o)return{why:`${a} is folded into ${o}; run ask unfold ${t} first`};const s=[];for(const w of await T(e)){const p=await g(w.file,null);for(const f of Array.isArray(p?.items)?p.items:[])f.from_ask===t&&s.push({key:`${w.slug}/${f.id}`,it:f})}const c=s.find(({it:w})=>Te(w)||Je(w));return c?{why:`${a} became ${c.key}, which has started`}:s.length?{items:s.map(w=>w.key)}:(await Promise.all(["html","json"].map(w=>A.stat($.join(e,"data","replies",`${t}.${w}`)).then(()=>!0,()=>!1)))).some(Boolean)||!!i||n.some(w=>w.kind==="ask-done"&&w.data?.id===t)||(await K(U(e,`ask:${t}`))).some(w=>(w.who||"claude")==="claude"&&w.kind!=="status")?{why:`${a} is not New: Claude already answered it`}:null}r(zo,"askNotNew");async function Ko(e,t,n){const a=/^uploads\/([\w.-]+)$/.exec(n?.url||"")?.[1],i=ma.test(a||"")?$.join(De(e),a):null;if(!i||!await A.stat(i).then(l=>l.isFile(),()=>!1))return{name:String(n?.name||a||"file")};await A.mkdir($.dirname(We(t,"x")),{recursive:!0});const o=a.replace(/^\d+-/,"");let s;for(let l=Date.now();s=`${l}-${o}`,!await A.copyFile(i,$.join(De(t),s),Ai.COPYFILE_EXCL).then(()=>!0,w=>w.code==="EEXIST"?!1:Promise.reject(w));l++);const c=await g(We(e,a),null);return c&&await y(We(t,s),c),{...n,url:`uploads/${s}`,path:$.join(De(t),s)}}r(Ko,"copyUpload");async function Hr(e,t,n){P(e),ge(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(O=>O.id===t&&O.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`),o=de(),s=o.find(O=>O.route===n)||d(`--to ${n}: no board at /${n} in the hub's manifest; the boards are ${o.map(O=>O.route).join(", ")||"none"}`);he(s.dir,e)&&d(`--to ${n}: that is this board`);const c=await zo(e,t,a);c?.why&&d(c.why);const l=a.filter(O=>O.kind==="ask-edited"&&O.data?.id===t&&typeof O.data?.text=="string").sort((O,B)=>String(O.ts).localeCompare(String(B.ts))),u=l.at(-1),w=[];for(const O of[...i.data?.files||[],...l.flatMap(B=>B.data.files||[])])w.push(await Ko(e,s.dir,O));const p={route:$.basename(e),title:(await te(e)).title,id:t},f=u?u.data.text:i.data?.text||"",m=await re(s,"ask",{text:f,...w.length?{files:w}:{},from:p}),h={route:n,title:s.title,id:m},k=new Date().toISOString(),x=await Nt(e);await y(Rt(e),{asks:{...x,[t]:{state:"done",at:k,moved:h}}});const E=`/${n}/#item=${encodeURIComponent(`ask:${m}`)}`;await V(U(e,`ask:${t}`),{ts:k,who:"claude",text:`Moved to ${s.title}.`,link:E});for(const O of c?.items||[])await fe(e,O,{done:!0,force:!0,text:`Closed: its ask moved to ${s.title}.`});return{file:$.join(s.dir,"data","events.jsonl"),lines:[`moved to ${E}`,...(c?.items||[]).map(O=>`closed: ${O}`)]}}r(Hr,"moveAsk");async function zr(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"),ba(e,await ka(e,t,"review record"),{state:n,text:a})}r(zr,"recordReview");async function ka(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(s=>s.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||ne(e,{})}}r(ka,"incomingPR");async function ba(e,{number:t,title:n,author:a,url:i,touches:o=[],asked:s=!1,head:c},{state:l,text:u,review_url:w}){const p=$.join(e,"data","reviews.json"),f=await g(p,{});return f[t]={number:Number(t),title:n,author:a,url:i,touches:o,asked:s,head:c,state:l,text:u,...w?{review_url:w}:{},at:new Date().toISOString()},await y(p,f),{file:p,lines:[]}}r(ba,"saveReview");async function Kr(e,t,{words:n,readAt:a}){P(e);const i=await ka(e,t,"review post"),o=String(n??"").trim()||d("review post needs the review text, as --file"),s=`${tt}Claude review from the ${(await te(e)).title} board`;(o.startsWith(tt.trim())||o.includes("Claude review from")||o.includes(dn))&&d(`review post adds the ${tt.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=_e(await te(e)).github_login;try{const u=await fo({repo:c,pr:i.number,since:a});u.length&&d(`PR #${t} got ${u.length===1?"a review":`${u.length} reviews`} after ${a} (${u.map(p=>`${p.login} at ${p.at}`).join(", ")}); reread the PR and review again`);const w=await uo({repo:c,pr:i.number,head:i.head,body:`${s}
|
|
10
|
+
`).replace(/<[^>]*>/g,"").replace(/&(?:#(x[\da-f]{1,6}|\d{1,7})|(\w+));/gi,(t,n,a)=>{const i=n&&parseInt(n.replace(/^x/i,""),/^x/i.test(n)?16:10);return n?i<=1114111?String.fromCodePoint(i):t:X(us,a.toLowerCase())??t})}r(fs,"htmlText");function ws(e){if(!e||!e.startsWith("/"))return e;const t=new URL(e,"http://board");return/^#(?:selectedCard|item)=[^&]+$/.test(t.hash)&&(t.hash=`${t.hash}&reply`),/\/[\w-]+\.html$/.test(t.pathname)&&!t.pathname.endsWith("/index.html")&&t.searchParams.set("from","push"),t.pathname+t.search+t.hash}r(ws,"pushUrl");const ps=r((e,t)=>`/${m.basename(e)}/#selectedCard=${encodeURIComponent(t)}&reply`,"threadUrl");async function va(e,t,n){const a=ls(n);if(!a)return[];const i=lo(t),o=await $o(i,s=>[a,cs.exec(s)?.[1]].filter(Boolean).join(" \xB7 "),ps(e,t));return o===null?[]:[`queued push ${i} now reads: ${o}`]}r(va,"freshenPush");const _a=r(e=>m.join(e,"data","hidden.json"),"hiddenFile"),hs=r(async e=>new Set((await $(_a(e),{events:[]})).events||[]),"readHidden");let Sa=Promise.resolve();function fc(e,t,n){Pe(t,"the event id");const a=Sa.then(async()=>{const i=await hs(e);n?i.add(t):i.delete(t),await y(_a(e),{events:[...i]})});return Sa=a.catch(()=>{}),a}r(fc,"setHidden");const ja=r(e=>m.join(e,"data","read.json"),"readMarksFile"),ms=r(async e=>(await $(ja(e),{})).cards||{},"readMarks");let Oa=Promise.resolve();function wc(e,t,n={}){const a=Oa.then(async()=>{const i=await ms(e),o=new Date().toISOString(),s=r((d,u)=>String(u||"")>d?u:d,"later"),c=Object.fromEntries([...new Set(t)].sort().map(d=>[d,s(i[d]||o,n[d])]));JSON.stringify(c)!==JSON.stringify(i)&&await y(ja(e),{cards:c})});return Oa=a.catch(()=>{}),a}r(wc,"markRead");const jn=20*1024*1024,qe=r(e=>m.join(e,"data","uploads"),"uploadsDir"),Ve=r((e,t)=>m.join(qe(e),".meta",`${t}.json`),"uploadMeta"),Pa=/^\d+-[\w.-]+$/,Da=/^[\w.+-]+\/[\w.+-]+$/,Aa=r(()=>Object.assign(new re(`a file can be at most ${jn/1024/1024} MB`),{status:413}),"tooLarge");async function pc(e,t,{name:n,type:a,length:i}){if(A(e),i>jn)throw Aa();const o=String(n||"").trim().slice(0,200)||"file",s=m.basename(o).replace(/[^\w.-]+/g,"-").replace(/^[.-]+/,"").slice(-80)||"file",c=String(a||"").split(";")[0].trim().toLowerCase(),d=Da.test(c)?c:"application/octet-stream";await P.mkdir(m.dirname(Ve(e,"x")),{recursive:!0});let u,p;for(let h=Date.now();!p;h++)u=`${h}-${s}`,p=await P.open(m.join(qe(e),u),"wx").catch(g=>g.code==="EEXIST"?null:Promise.reject(g));const w=m.join(qe(e),u);let f=0;try{for await(const h of t){if(f+=h.length,f>jn)throw Aa();await p.write(h)}}catch(h){throw await p.close(),await P.rm(w,{force:!0}),h}return await p.close(),await y(Ve(e,u),{name:o,type:d,size:f,at:new Date().toISOString()}),{name:o,url:`uploads/${u}`,path:w,type:d,size:f}}r(pc,"saveUpload");async function hc(e,t){if(!Pa.test(t||""))return null;const n=m.join(qe(e),t),a=await P.stat(n).catch(()=>null);if(!a?.isFile())return null;const i=await $(Ve(e,t),{});return{path:n,name:i.name||t,type:Da.test(i.type||"")?i.type:"application/octet-stream",size:a.size}}r(hc,"readUpload");const Ra=["github","slack"];async function U(e,t,{text:n,link:a,log:i,who:o="claude",via:s,kind:c,by:d,command:u,card:p}){A(e),String(t).startsWith("ask:")?await ot(e,t.slice(4),"the ask"):await x(e,t),/^[^\n]{1,40}$/.test(o.trim())||l("--who takes claude, love or a reviewer's name"),s!==void 0&&!Ra.includes(s)&&l(`--via takes ${Ra.join(" or ")}`);const w={ts:new Date().toISOString(),who:o.trim(),text:S(n,"--text"),link:ne(a,h=>ba(h,"--link")),log:i,via:s,kind:c,by:d,command:u,card:p},f=M(e,t);return await z(f,w),{file:f,lines:w.who==="claude"&&!s?await va(e,t,w.text):[]}}r(U,"appendThread");const ke=r(e=>m.join(e,"data","ask-links.json"),"askLinksFile");async function De(e){return(await $(ke(e),{})).links||{}}r(De,"readAskLinks");async function K(e,t){const{tickets:n}=be(await ie(e)),a=await F(e),i=/^ask:(.+)$/.exec(t)?.[1];if(i===void 0){const s=/^([^/\s]+)\/([^/\s]+)$/.exec(t);if(!s)return t;const c=a.find(p=>p.slug===s[1]),d=c&&await $(c.file,null);if(!d)return s[2];const u=(d.items||[]).find(p=>p.id===s[2]);return Qe(d,u||{id:s[2]},n)}const o=e?X(await De(e),i):void 0;if(o)return K(e,o);for(const s of a){const c=await $(s.file,null),d=(Array.isArray(c?.items)?c.items:[]).find(u=>u.from_ask===i);if(d)return Qe(c,d,n)}return`task ${i}`}r(K,"cardName");async function mc(e,t){const n=(await G(m.join(e,"data","events.jsonl"))).find(a=>a.id===t);return n?.kind==="ask"?K(e,`ask:${t}`):typeof n?.data?.key=="string"?K(e,n.data.key):`the ${n?.kind} event`}r(mc,"eventCard");async function gc(e,t,n,a,i){A(e),Pe(t,"the event id"),(await G(m.join(e,"data","events.jsonl"))).some(d=>d.id===t&&d.kind==="ask")||l(`no ask "${t}" in this site's data/events.jsonl`),await Mt(e,t),await x(e,n);const s={ts:new Date().toISOString(),who:"claude",text:S(a,"--summary")};i!==void 0&&S(i,"--title");const c=await De(e);return await y(ke(e),{links:{...c,[t]:n}}),await z(M(e,n),s),i!==void 0&&await Ea(e,t,i),ke(e)}r(gc,"relateAsk");const Na=r(e=>m.join(e,"data","ask-titles.json"),"askTitlesFile");async function gs(e){return(await $(Na(e),{})).titles||{}}r(gs,"readAskTitles");async function Ea(e,t,n){await y(Na(e),{titles:{...await gs(e),[t]:S(n,"--title")}})}r(Ea,"titleAsk");async function $c(e,t){await ot(e,t,"the ask");const n=await De(e);return X(n,t)===void 0&&l(`ask "${t}" is not folded into a card`),delete n[t],await y(ke(e),{links:n}),ke(e)}r($c,"unfoldAsk");const qt=r(e=>m.join(e,"data","ask-progress.json"),"askProgressFile");async function Ft(e){return(await $(qt(e),{})).asks||{}}r(Ft,"readAskProgress");async function Mt(e,t){const n=X(await Ft(e),t)?.moved;n&&l(`ask "${t}" was moved to /${n.route}; it lives there now`)}r(Mt,"refuseMoved");async function kc(e,t,n,a){await ot(e,t,"the ask"),await Mt(e,t);const i=await Ft(e);return await y(qt(e),{asks:{...i,[t]:{state:n,at:new Date().toISOString(),...a===void 0?{}:{next:S(a,"--next")}}}}),qt(e)}r(kc,"markAsk");async function $s(e,t,n){const a=`ask "${t}"`;if(n.some(p=>p.kind==="ask-withdrawn"&&p.data?.id===t&&!p.data?.key))return{why:`${a} was deleted by the owner`};const i=X(await Ft(e),t);if(i?.moved)return{why:`${a} was already moved to /${i.moved.route}`};const o=X(await De(e),t);if(o)return{why:`${a} is folded into ${o}; run ask unfold ${t} first`};const s=[];for(const p of await F(e)){const w=await $(p.file,null);for(const f of Array.isArray(w?.items)?w.items:[])f.from_ask===t&&s.push({key:`${p.slug}/${f.id}`,it:f})}const c=s.find(({it:p})=>Ce(p)||Ne(p));return c?{why:`${a} became ${c.key}, which has started`}:s.length?{items:s.map(p=>p.key)}:(await Promise.all(["html","json"].map(p=>P.stat(m.join(e,"data","replies",`${t}.${p}`)).then(()=>!0,()=>!1)))).some(Boolean)||!!i||n.some(p=>p.kind==="ask-done"&&p.data?.id===t)||(await G(M(e,`ask:${t}`))).some(p=>(p.who||"claude")==="claude"&&p.kind!=="status")?{why:`${a} is not New: Claude already answered it`}:null}r($s,"askNotNew");async function ks(e,t,n){const a=/^uploads\/([\w.-]+)$/.exec(n?.url||"")?.[1],i=Pa.test(a||"")?m.join(qe(e),a):null;if(!i||!await P.stat(i).then(d=>d.isFile(),()=>!1))return{name:String(n?.name||a||"file")};await P.mkdir(m.dirname(Ve(t,"x")),{recursive:!0});const o=a.replace(/^\d+-/,"");let s;for(let d=Date.now();s=`${d}-${o}`,!await P.copyFile(i,m.join(qe(t),s),eo.COPYFILE_EXCL).then(()=>!0,p=>p.code==="EEXIST"?!1:Promise.reject(p));d++);const c=await $(Ve(e,a),null);return c&&await y(Ve(t,s),c),{...n,url:`uploads/${s}`,path:m.join(qe(t),s)}}r(ks,"copyUpload");async function bc(e,t,n){A(e),Pe(t,"the event id"),(typeof n!="string"||!n.trim())&&l("ask move needs --to <route>, the board to move it to");const a=await G(m.join(e,"data","events.jsonl")),i=a.find(O=>O.id===t&&O.kind==="ask")||l(`no ask "${t}" in this site's data/events.jsonl`),o=ge(),s=o.find(O=>O.route===n)||l(`--to ${n}: no board at /${n} in the hub's manifest; the boards are ${o.map(O=>O.route).join(", ")||"none"}`);je(s.dir,e)&&l(`--to ${n}: that is this board`);const c=await $s(e,t,a);c?.why&&l(c.why);const d=a.filter(O=>O.kind==="ask-edited"&&O.data?.id===t&&typeof O.data?.text=="string").sort((O,B)=>String(O.ts).localeCompare(String(B.ts))),u=d.at(-1),p=[];for(const O of[...i.data?.files||[],...d.flatMap(B=>B.data.files||[])])p.push(await ks(e,s.dir,O));const w={route:m.basename(e),title:(await ie(e)).title,id:t},f=u?u.data.text:i.data?.text||"",h=await me(s,"ask",{text:f,...p.length?{files:p}:{},from:w}),g={route:n,title:s.title,id:h},k=new Date().toISOString(),v=await Ft(e);await y(qt(e),{asks:{...v,[t]:{state:"done",at:k,moved:g}}});const D=`/${n}/#selectedCard=${encodeURIComponent(`ask:${h}`)}`;await z(M(e,`ask:${t}`),{ts:k,who:"claude",text:`Moved to ${s.title}.`,link:D});for(const O of c?.items||[])await pe(e,O,{done:!0,force:!0,text:`Closed: its ask moved to ${s.title}.`});return{file:m.join(s.dir,"data","events.jsonl"),lines:[`moved to ${D}`,...(c?.items||[]).map(O=>`closed: ${O}`)]}}r(bc,"moveAsk");async function yc(e,t,{state:n,text:a}={}){return A(e),["posted","needs"].includes(n)||l("review record needs --state posted or --state needs"),String(a??"").trim()||l("review record needs the review text, as --text or --file"),Ba(e,await Ia(e,t,"review record"),{state:n,text:a})}r(yc,"recordReview");async function Ia(e,t,n){/^[1-9]\d{0,6}$/.test(String(t))||l(`${n} takes the PR number, such as 437, not "${t}"`);const a=await $(m.join(e,"data","incoming.json"),{});return{...(a?.prs||[]).find(s=>s.number===Number(t))||(await $(m.join(e,"data","reviews.json"),{}))[t]||l(`PR #${t} is not an incoming PR on this board`),repo:a?.repo||oe(e,{})}}r(Ia,"incomingPR");async function Ba(e,{number:t,title:n,author:a,url:i,touches:o=[],asked:s=!1,head:c},{state:d,text:u,review_url:p}){const w=m.join(e,"data","reviews.json"),f=await $(w,{});return f[t]={number:Number(t),title:n,author:a,url:i,touches:o,asked:s,head:c,state:d,text:u,...p?{review_url:p}:{},at:new Date().toISOString()},await y(w,f),{file:w,lines:[]}}r(Ba,"saveReview");async function xc(e,t,{words:n,readAt:a}){A(e);const i=await Ia(e,t,"review post"),o=String(n??"").trim()||l("review post needs the review text, as --file"),s=`${pt}Claude review from the ${(await ie(e)).title} board`;(o.startsWith(pt.trim())||o.includes("Claude review from")||o.includes(yn))&&l(`review post adds the ${pt.trim()} header and the claude-bridge marker itself; pass only your words`);const c=i.repo||l("review post needs a board on GitHub; this board has no repo there");Number.isNaN(Date.parse(a))&&l("--read-at takes the time you read the PR, such as 2026-11-05T09:30:00Z");const d=be(await ie(e)).github_login;try{const u=await Co({repo:c,pr:i.number,since:a});u.length&&l(`PR #${t} got ${u.length===1?"a review":`${u.length} reviews`} after ${a} (${u.map(w=>`${w.login} at ${w.at}`).join(", ")}); reread the PR and review again`);const p=await Lo({repo:c,pr:i.number,head:i.head,body:`${s}
|
|
11
11
|
|
|
12
12
|
${o}
|
|
13
13
|
|
|
14
|
-
${
|
|
15
|
-
`),a})}r(
|
|
16
|
-
`)}r(
|
|
17
|
-
`)}r(Go,"linkText");const va=r(e=>!!e?.approved||e?.reviewDecision==="APPROVED","approved");async function ec(e,t){P(e);const n=await aa.waiting("slack");if(n)return{waiting:n,links:[],replies:[],reactions:[],digest:null};const a=await te(e),i=await g(on,{}),o=await Lt(e),s=[];for(const w of await T(e)){const p=await g(w.file,{}),f=pn(p,a.reviewers);for(const m of p.items||[])if(m.lane!=="done")for(const h of Ae(m,ne(e,p))){const k=nt(h),x=Z(i.prs,k);if(!(x?.state!=="OPEN"||x.isDraft))for(const E of f)st(o[k]?.links?.[E.name])||s.some(O=>O.pr===k&&O.reviewer===E.name)||s.push({pr:k,key:`${w.slug}/${m.id}`,reviewer:E.name,slack_user:E.slack_user,head:x.head||null,text:Go(x,h,m)})}}const c=[],l=[];for(const[w,p]of Object.entries(o)){const f=Z(i.prs,w),m=[va(f)&&"git_approved",f?.state==="MERGED"?"git_merged":f?.state==="CLOSED"&&"x"].filter(Boolean);for(const[h,k]of Object.entries(p.links))if(k.ts)for(const x of m)k.reactions.includes(x)||l.push({pr:w,reviewer:h,channel:k.channel,ts:k.ts,emoji:x});for(const h of p.replies)for(const[k,x]of Object.entries(h.to))st(x)||c.push({pr:w,id:h.id,reviewer:k,channel:p.links[k].channel,thread_ts:p.links[k].ts,text:h.text})}const u=await Qo(e,t);return{links:s,replies:c,reactions:l,digest:u.due?u:null}}r(ec,"reviewDue");async function tc(e,t,n){P(e);const a=j(n,"what changed"),{wave:i,item:o}=await v(e,t),s=[];for(const l of Ae(o,ne(e,i)))s.push(...await xa(e,nt(l),"updated",`Updated: ${a}`));const c=[...new Set(s)];return{lines:[c.length?`Updated queued for ${c.join(", ")} on ${t}`:`no reviewer has a link to ${t}'s PRs; nothing queued`]}}r(tc,"reviewUpdated");async function nc(e,t){const{wave:n,item:a}=await v(e,t),i=await Lt(e);return Ae(a,ne(e,n)).some(o=>Object.values(i[nt(o)]?.links||{}).some(s=>s.ts))}r(nc,"cardLinked");const _a=r(e=>$.join(P(e),"data","review-digests.json"),"digestFile");async function Qo(e,{now:t=new Date}={}){P(e);const n=await te(e),a=await g(on,{}),i=await Lt(e),o=mo(),s=new Map;for(const f of await T(e)){const m=await g(f.file,{}),h=pn(m,n.reviewers);for(const k of h)s.has(k.name)||s.set(k.name,{name:k.name,slack_user:k.slack_user,dm:k.dm,items:[]});for(const k of m.items||[]){if(k.lane==="done")continue;const x=`${f.slug}/${k.id}`,E=wo(m,k,x,o);for(const O of Ae(k,ne(e,m))){const B=nt(O),_=Z(a.prs,B);if(!(_?.state!=="OPEN"||_.isDraft||_.failing?.length||va(_)))for(const S of h){const D=i[B]?.links?.[S.name]?.ts||Z(E,S.name)?.ts||null,F=D?(t.getTime()-Number(D)*1e3)/864e5:null,M=F>2?Math.floor(F):null;s.get(S.name).items.push({key:x,pr:B,title:k.title,url:fn(O),sent_ts:D,...M?{days_old:M}:{},text:`${fn(O)} \xB7 ${k.title}${M?` (${M} days old)`:""}`})}}}}const{date:c,open:l}=ho(t),u=((await g(_a(e),{})).digests||[]).filter(f=>f.date===c),w=await aa.waiting("slack");for(const f of u)s.has(f.reviewer)||s.set(f.reviewer,{name:f.reviewer,items:[]});const p=[...s.values()].map(f=>{const m=u.find(k=>k.reviewer===f.name),h=m?!m.posting_at&&(!m.ts||m.replies.some(k=>!st(k))):f.items.length>0;return{...f,owed:h,...m?{sent:m}:{}}}).filter(f=>f.items.length||f.sent);return{date:c,due:!w&&l&&p.some(f=>f.owed),...w?{waiting:w}:{},reviewers:p}}r(Qo,"reviewDigest");async function ac(e,t,n,{channel:a,header:i,post:o}){const s=_a(e),c=r(w=>(w.digests||[]).find(p=>p.date===t&&p.reviewer===n.name),"entry");await Ne(s,w=>{c(w)||(w.digests=[...(w.digests||[]).filter(p=>p.date===t),{date:t,reviewer:n.name,channel:a,replies:n.items.map(p=>({pr:p.pr,text:p.text}))}],delete w.last_date)});const l=c(await g(s,{})),u=l.ts||await It(s,c,()=>o({channel:l.channel,text:i(l.replies.length)}));for(const[w,p]of l.replies.entries())st(p)||await It(s,f=>c(f).replies[w],()=>o({channel:l.channel,thread_ts:u,text:p.text}))}r(ac,"postDigest");function ic(e,t){const n=$n(Array.isArray(t)?t:[t],"the reviewer file");return kn(e,a=>[...a.map(i=>n.find(o=>ot(o.name,i.name))||i),...n.filter(i=>!a.some(o=>ot(i.name,o.name)))])}r(ic,"addReviewers");function oc(e,t){return kn(e,n=>{const a=t.filter(i=>!n.some(o=>ot(o.name,i)));return a.length&&d(`this board has no reviewer ${a.map(i=>`"${i}"`).join(", ")}`),n.filter(i=>!t.some(o=>ot(i.name,o)))})}r(oc,"removeReviewers");const Zo=r(()=>$.join(Xt.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),bn=r(e=>{try{return e()}catch(t){throw t instanceof bo&&d(t.message),t}},"ruleCall"),yn=r(async e=>oa(await g($.join(e,"board.json"),{})),"readBoardRules");let Sa=Promise.resolve();function rt(e,t){const n=Sa.then(async()=>{const a=$.join(P(e),"board.json"),i=await g(a,{}),{set:o,...s}=t(i);return await y(a,{...i,...o}),{file:a,...s}});return Sa=n.catch(()=>{}),n}r(rt,"writeBoardJSON");const ja=r((e,t)=>rt(e,n=>{const a=bn(()=>t(oa(n)));return{set:{rules:a.rules},...a}}),"writeBoardRules");function sc(e,t){const n=Object.keys(t||{}).sort().join(),a=n==="level,login",i=a?t.login:t?.[n],o=typeof i=="string"?i.trim():null;(!["add","remove","level,login"].includes(n)||!Oo(o))&&d("Send add or remove with one login, like name@github, or a listed login with its level."),a&&!["view","full"].includes(t.level)&&d("A level is view or full.");const s=r(({login:c,level:l})=>l==="full"?{login:c,level:l}:c,"stored");return rt(e,c=>{const l=jo(c);a&&!l.some(p=>p.login===o)&&d(`${o} isn't shared with yet. Add them first.`);const w=(n==="remove"?l.filter(p=>p.login!==o):a?l.map(p=>p.login===o?{login:o,level:t.level}:p):l.some(p=>p.login===o)?l:[...l,{login:o,level:"view"}]).map(s);return{set:{share:w},share:w}}).then(({share:c})=>({share:c}))}r(sc,"saveBoardShare");const Oa=r(e=>typeof e=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e),"validGithubLogin"),Pa=r(e=>{try{return e?cn(e):null}catch{return null}},"storedTickets"),xn=r(e=>Number.isInteger(e)&&e>=1&&e<=99,"validCap"),vn=["max_implementers","max_planners"],_e=r((e,t)=>{const n=Pa(e?.tickets);return{auto_merge:e?.auto_merge===!0,auto_build:e?.auto_build===!0,auto_pull:e?.auto_pull===!0,max_implementers:xn(e?.max_implementers)?e.max_implementers:Ni,max_planners:xn(e?.max_planners)?e.max_planners:Ei,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}:{},...Oa(e?.github_login)?{github_login:e.github_login}:{},...ia(e?.build_target)?{build_target:e.build_target}:{}}},"settingsOf");async function rc(e,t,n=new Date){let a;try{a=zi(t,n)}catch(i){d(i.message)}return await rt(e,()=>({set:{paused_until:a??void 0}})),a}r(rc,"setBoardPause");async function Vo(e){const t=P(e);return _e(await g($.join(t,"board.json"),{}),de().find(n=>he(n.dir,t)))}r(Vo,"readBoardSettings");const cc=r((e,t)=>!!e?.github&&(!e.local||e.hub===!0)&&t.prs,"opensPRs");async function He(e,t){Fe(t,["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","tickets","github_login","build_target"],"the settings"),Object.keys(t).length||d("the settings name nothing to change"),t={...t};for(const[n,a]of Object.entries(t))if(n==="tickets"){try{t.tickets=cn(a)}catch(o){d(o.message)}t.tickets.source==="linear"&&!no()&&d(eo);const i=t.tickets.source==="github"&&e?de().find(o=>he(o.dir,e)):null;i&&!i.github&&d("GitHub issues need the board\u2019s repo on GitHub, and this board\u2019s repo isn\u2019t; pick internal or linear")}else n==="github_login"?a===""||a===null?t.github_login=void 0:Oa(a)||d("github_login must be a GitHub login"):n==="build_target"?a===null?t.build_target=void 0:ia(a)||d('build_target must name .kickflip/build-<target>: letters, digits, ".", "_" or "-", or null for .kickflip/build'):vn.includes(n)?xn(a)||d(`${n} must be a whole number from 1 to 99`):Dt(a,n);if(t.prs===!1){const n=e?de().find(a=>he(a.dir,e)):null;n?.github&&!n.local&&!(await go(n.owner?.repo)).pushMain&&d("Merge lands each card on main and pushes it, so turn on Push main after a land first")}return rt(e,n=>{const a=e?de().find(s=>he(s.dir,e)):null,i=_e(n,a),o=_e({...n,...t},a);return{set:t,settings:o,changed:Object.keys(t).filter(s=>JSON.stringify(i[s])!==JSON.stringify(o[s]))}})}r(He,"saveBoardSettings");async function lc(){if(to().set)return[];const e=[];for(const t of de()){if((await Vo(t.dir)).tickets?.source!=="linear")continue;const{settings:n}=await He(t.dir,{tickets:{source:"internal"}});await re(t,"settings",{tickets:n.tickets}),e.push(`/${t.route} fell back to internal tickets: Hub has no Linear key`)}return e}r(lc,"fallBackFromLinear");async function dc(e,t,n){const a=String(t).replace(/-/g,"_"),i=["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","github_login","tickets","build_target"];if(i.includes(a)||d(`no board setting "${t}"; settings are ${i.map(c=>c.replace(/_/g,"-")).join(", ")}`),a==="tickets"){let c;try{c=JSON.parse(n)}catch{d('settings set tickets takes the source as JSON, such as {"source":"github"}')}const{file:l,settings:u}=await He(e,{tickets:c});return{file:l,lines:Yo({tickets:u.tickets})}}if(a==="github_login"){const{file:c,settings:l}=await He(e,{github_login:n});return{file:c,lines:[`github-login ${l.github_login||"unset"}`]}}if(a==="build_target"){const{file:c,settings:l}=await He(e,{build_target:n==="default"?null:n});return{file:c,lines:[`build-target ${l.build_target||"default"}`]}}if(vn.includes(a)){const{file:c,settings:l}=await He(e,{[a]:/^\d+$/.test(n)?Number(n):n});return{file:c,lines:[`${t} ${l[a]}`]}}["on","off"].includes(n)||d(`settings set ${t} takes on or off, not "${n}"`);const{file:o,settings:s}=await He(e,{[a]:n==="on"});return{file:o,lines:[`${t} ${s[a]?"on":"off"}`]}}r(dc,"setBoardSetting");const Yo=r(e=>Object.entries(e).map(([t,n])=>`setting ${t.replace(/_/g,"-")} ${t==="tickets"?JSON.stringify(n):t==="github_login"||vn.includes(t)?n:t==="build_target"?n||"default":n?"on":"off"}`),"settingsLines");async function _n(e,t,n){const a=t?bn(()=>n(Pt())):await ja(e,n);return t&&bn(()=>So(a.rules)),{file:t?Zo():a.file,lines:[`id ${a.rule.id}`]}}r(_n,"changeRules");async function Aa(e,t,n){const a=r(s=>s.map(c=>c.id),"ids");n||P(e);const[i,o]=n?[a(Pt()),e?a(await yn(e)):[]]:[a(await yn(e)),a(Pt())];i.includes(t)||d(`no ${n?"hub":"board"} rule "${t}"${o.includes(t)?`; it is a ${n?"board":"hub"} rule, so ${n?"drop":"pass"} --hub`:""}`)}r(Aa,"checkRuleId");async function uc(e){const t=r(n=>a=>`${n} ${a.id} ${a.by==="claude"?"claude":"owner"} ${a.text}`,"line");return{lines:[...Pt().map(t("hub")),...e?(await yn(e)).map(t("board")):[]]}}r(uc,"listRules");function fc(e,{text:t,hub:n=!1,by:a}){return n||P(e),_n(e,n,i=>yo(i,{text:t,by:a}))}r(fc,"addRuleTo");async function pc(e,t,{text:n,hub:a=!1}){return await Aa(e,t,a),_n(e,a,i=>xo(i,t,n))}r(pc,"editRuleIn");async function wc(e,t,{hub:n=!1}){return await Aa(e,t,n),_n(e,n,a=>vo(a,t))}r(wc,"removeRuleFrom");const mc=r((e,t,n)=>ja(e,a=>_o(a,t,n)),"saveBoardRules");async function hc(e,{id:t,title:n,summary:a}){P(e);const i=await g(L(e),{topics:[],items:[]}),o=i.topics||[];return o.some(s=>s.id===j(t,"the topic id"))&&d(`topic "${t}" already exists`),o.push({id:t.trim(),title:j(n,"--title"),...a===void 0?{}:{summary:j(a,"--summary")}}),await y(L(e),{...i,topics:o,items:i.items||[]}),L(e)}r(hc,"addTopic");const Da=["quiet","info","notice","danger","success"];function Xo(e,t){if(e.diagram===void 0!=(e.practice===void 0)&&d(`${t} needs diagram and practice together`),e.diagram===void 0)return;const n=e.diagram;(!Array.isArray(n)||!n.length)&&d(`${t}.diagram must be a non-empty array of rows`),n.forEach((a,i)=>{(!Array.isArray(a)||!a.length||a.length>4)&&d(`${t}.diagram[${i}] must be a row of 1 to 4 boxes`),a.forEach((o,s)=>{const c=`${t}.diagram[${i}][${s}]`;Fe(o,["text","tone","to"],c),j(o.text,`${c}.text`),Y(o.tone,l=>Da.includes(l)?l:d(`${c}.tone takes ${Da.join(", ")}`)),Y(o.to,l=>Array.isArray(l)&&i+1<n.length&&l.every(u=>Number.isInteger(u)&&u>=0&&u<n[i+1].length)?l:d(`${c}.to must list boxes of the next row by index`))})}),(!Array.isArray(e.practice)||!e.practice.length)&&d(`${t}.practice must be a non-empty array of lines`),e.practice.forEach((a,i)=>j(a,`${t}.practice[${i}]`))}r(Xo,"checkDiagram");function es(e,t){(!Array.isArray(e)||!e.length)&&d(`${t}.options must be a non-empty array`);const n=new Set;e.forEach((a,i)=>{const o=`${t}.options[${i}]`;Fe(a,["id","label","recommended","detail","diagram","practice"],o),Xo(a,o),n.has(j(a.id,`${o}.id`))&&d(`${t} has two options with id "${a.id}"`),n.add(a.id),j(a.label,`${o}.label`).startsWith(`${a.id}. `)&&d(`${o}.label starts with "${a.id}. ", but the page adds the option letter itself`),Y(a.recommended,s=>Dt(s,`${o}.recommended`)),Y(a.detail,s=>typeof s=="string"?s:d(`${o}.detail must be text`))}),e.filter(a=>a.recommended).length>1&&d(`${t} recommends more than one option`)}r(es,"checkOptions");function ts(e,t,n){Fe(e,["option","card","answer"],n),t.some(a=>a.id===j(e.option,`${n}.option`))||d(`${n}.option names no option "${e.option}"`),j(e.card,`${n}.card`),j(e.answer,`${n}.answer`)}r(ts,"checkSuggested");const ze=r(e=>$.join(e,"data","prototypes.json"),"prototypesFile"),Sn=r((e,t,n)=>$.join(e,"public","prototypes",t,`${n}.js`),"takeFile");async function ct(e){const t=e?(await g(ze(e),{})).prototypes:null;return Array.isArray(t)?t:[]}r(ct,"readPrototypes");const ns=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 as(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=j(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(as,"prototypeTake");function is(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:j(t[2],`--state ${t[1]}'s label`)}}r(is,"prototypeState");const Ra=r(e=>e.find((t,n)=>e.findIndex(a=>a.key===t.key)!==n),"twiceIn"),Se=r(e=>/^prototype-(.+)\.html$/.exec($.posix.basename(String(e||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function gc(e,{topic:t,title:n,card:a,takes:i=[],served:o,states:s=[]}){P(e),ns(t),j(n,"--title"),i.length||d('prototype add needs a --take KEY:"Label" for each take');const c=i.map(as),l=Ra(c);l&&d(`--take ${l.key} is given twice`),o!==void 0&&!/^\/\S*$/.test(o)&&d(`--served takes the page's path on the served site, starting with "/", not "${o}"`),s.length&&o===void 0&&d("--state goes with --served: only a served round re-renders its states in place");const u=s.map(is),w=Ra(u);w&&d(`--state ${w.key} is given twice`),a!==void 0&&await v(e,a);const p=await ct(e);p.some(h=>h.topic===t)&&d(`prototype ${t} is already recorded; run prototype rm ${t} first, or pick another topic`),await A.stat($.join(e,"public",`prototype-${t}.html`)).then(()=>!0,()=>!1)&&d(`public/prototype-${t}.html is a hand-written page that questions may link; pick another topic`);const f={topic:t,title:n.trim(),...a===void 0?{}:{card:a},...o===void 0?{}:{served:o},takes:c,...u.length?{states:u}:{},at:new Date().toISOString()};await y(ze(e),{prototypes:[...p,f]});const m=`page prototype-${t}.html`;return o!==void 0?{file:ze(e),lines:[m,`served ${o} from the base prototype worktree (board.mjs prototype base)`,...u.length?[`states ${u.map(h=>h.key).join(", ")}`]:[]]}:{file:ze(e),lines:[m,...c.map(h=>`take ${h.key} ${Sn(e,t,h.key)}`)]}}r(gc,"addPrototype");async function $c(e){P(e);const t=await ct(e);return{lines:t.length?t.map(n=>[n.topic,n.title,n.takes.map(a=>`${a.key}: ${a.label}`).join(", "),n.card,n.served&&`served ${n.served}`,n.states&&`states ${n.states.map(a=>a.key).join(", ")}`].filter(Boolean).join(" \xB7 ")):["no prototypes recorded"]}}r($c,"listPrototypes");async function kc(e){const t=(await ct(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&&Se(i.url)===a))}r(kc,"servedRoundsOpen");async function bc(e,t){P(e);const n=await ct(e);n.some(o=>o.topic===t)||d(`no prototype "${t}" is recorded`);const i=((await g(L(e),{items:[]})).items||[]).filter(o=>!o.resolved&&Se(o.url)===t).map(o=>o.id);return i.length&&d(`prototype ${t} backs open question ${i.join(", ")}; resolve ${i.length===1?"it":"them"} first`),await y(ze(e),{prototypes:n.filter(o=>o.topic!==t)}),ze(e)}r(bc,"removePrototype");async function os(e,t,n){const a=t.url?.split(/[?#]/)[0];if(!a||!$.posix.basename(a).startsWith("prototype-"))return;const i=$.posix.basename(a),o=t.url.split("#")[0].split("?").slice(1).join("?"),s=`${n}.url`;new URLSearchParams(o).get("q")!==t.id&&d(`${s} must be ${i}?q=${t.id} so the pill can pick`);const c=Se(a),l=(await ct(e)).find(f=>f.topic===c);l||d(`${s} names ${i}, which is not a recorded prototype; record it with board.mjs prototype add ${c} --title "\u2026" --take A:"\u2026", then write its take files`);const u=l.takes.map(f=>f.key),w=t.options.map(f=>f.id),p=w.map(f=>u.indexOf(f));if(p.some((f,m)=>f<0||m>0&&f<p[m-1])&&d(`${n}.options ids ${w.join(", ")} must be a non-empty subset of ${i}'s takes, in take order: ${u.join(", ")}`),!l.served)for(const f of u)await A.stat(Sn(e,c,f)).then(m=>m.isFile(),()=>!1)||d(`${s} take ${f} has no file yet; write ${Sn(e,c,f)}`)}r(os,"checkPrototype");async function yc(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:[]}),o=i.items||[],s=await g(it(e),{}),c=new Set([...o,...Object.values(s).flat()].map(p=>p.id)),l=new Set((i.topics||[]).map(p=>p.id));for(const[p,f]of a.entries()){const m=`question ${f?.id??p+1}`;Fe(f,["id","topic","source","question","context","url","urgent","options","round_of","reask_of","ask","suggested","prototype_gate"],m),c.has(ge(f.id,`${m}.id`))&&d(`question id "${f.id}" is already used`),c.add(f.id),j(f.topic,`${m}.topic`),f.topic.startsWith("item:ask:")?await lt(e,f.topic.slice(9),`${m}.topic`):f.topic.startsWith("item:")?await v(e,f.topic.slice(5)):f.topic.startsWith("lineup:")?await Kt(e,f.topic.slice(7)):f.topic.startsWith("wave:")?await Cs(e,f.topic.slice(5)):l.has(f.topic)||d(`topic "${f.topic}" is not declared; add it with: topic add ${f.topic} --title <t>`),j(f.question,`${m}.question`);for(const h of["source","context"])Y(f[h],k=>j(k,`${m}.${h}`));Y(f.url,h=>ra(h,`${m}.url`)),Y(f.urgent,h=>Dt(h,`${m}.urgent`)),es(f.options,m),Y(f.prototype_gate,h=>ss(h,f,m)),Y(f.suggested,h=>ts(h,f.options,`${m}.suggested`)),await os(e,f,m),f.ask!==void 0&&await Qs(e,f,m),Y(f.round_of,h=>{const k=o.find(x=>x.id===h);k||d(`${m}.round_of names no question "${h}" in owner.json`),k.topic!==f.topic&&d(`${m}.round_of names ${h} on topic ${k.topic}; a round stays on its question's topic, ${f.topic}`),Se(f.url)||d(`${m}.round_of needs a url on a recorded prototype, prototype-<topic>.html?q=${f.id}`)}),Y(f.reask_of,h=>{const k=o.find(x=>x.id===h);k||d(`${m}.reask_of names no question "${h}" in owner.json`),k.topic!==f.topic&&d(`${m}.reask_of names ${h} on topic ${k.topic}; a re-ask stays on its question's topic, ${f.topic}`),k.resolved||d(`${m}.reask_of names ${h}, which is not resolved; only a resolved question is asked again`)})}for(const p of new Set(a.map(f=>Se(f.url)).filter(Boolean)))await n(e,p);const u=[],w=[];for(const p of a){const f=p.topic.startsWith("item:")&&!p.topic.startsWith("item:ask:")?p.topic.slice(5):null,m=f&&await Na(e,f);m&&((s[f]||=[]).push(p),w.push(p),u.push(`staged: ${p.id} waits for ${f}'s ${m} to finish; the card's staged questions then publish together`))}return w.length&&await y(it(e),s),w.length===a.length?{file:it(e),lines:u,staged:w.length}:Ea(e,i,o,a.filter(p=>!w.includes(p)),u)}r(yc,"addQuestions");async function Na(e,t){const{source:n}=await v(e,t);for(const a of await z(n.dir,N(t).id)){const i=await g(a,null);if(i?.started&&!i.done&&Ce(i)&&i.kind!==le)return i.kind}return null}r(Na,"runningPlanner");async function Bt(e,t){if(!e||await Na(e,t))return;const n=await g(it(e),{});if(!n[t])return;const{[t]:a,...i}=n,o=await g(L(e),{topics:[],items:[]});await Ea(e,o,o.items||[],a,[]),await y(it(e),i)}r(Bt,"publishStaged");async function Ea(e,t,n,a,i){const{answers:o,requests:s,reopens:c}=await Ke(e),l=new Date().toISOString(),u=[],w=[];for(const f of a.filter(m=>m.round_of))f.round=an(n.find(m=>m.id===f.round_of))+1,je(t,[f.round_of],`Followed by prototype round ${f.round}`,l),w.push(f.round_of);for(const f of a.filter(m=>m.reask_of))je(t,[f.reask_of],`Asked again as ${f.id}`,l),w.push(f.reask_of);for(const f of a){const m=Se(f.url),h=n.filter(x=>x.topic===f.topic).map(x=>Pe(x,o,s,[],c)),k=f.topic.startsWith("item:")?Ca(h):[];for(const x of h)k.includes(x.id)?(je(t,[x.id],`Followed by ${f.id}`,l),w.push(x.id)):!x.resolved&&m&&Se(x.url)===m&&(je(t,[x.id],`Replaced by ${f.id}`,l),u.push(x.id))}const p={...t,topics:t.topics||[],items:[...n,...a.map(f=>({...f,asked_at:l}))]};await y(L(e),p);for(const f of new Set(a.map(m=>Ia(m.topic)).filter(Boolean)))await ds(e,f);return{file:L(e),lines:[...i,...u.map(f=>`replaced: ${f}`),...w.map(f=>`resolved: ${f}`)]}}r(Ea,"publishQuestions");function ss(e,t,n){Dt(e,`${n}.prototype_gate`),e&&((!t.topic.startsWith("item:")||t.topic.startsWith("item:ask:"))&&d(`${n}.prototype_gate goes on a card's topic, item:<wave>/<id>`),t.options?.map(a=>a.id).join()!=="yes,no"&&d(`${n}.prototype_gate takes exactly the options yes and no, in that order`))}r(ss,"checkGate");async function xc(e,t){const n=await g(L(e),{topics:[],items:[]}),a=new Map((n.items||[]).map(c=>[c.id,c])),i=[];for(const c of t){const l=a.get(String(c.id));if(!l?.prototype_gate||!["yes","no"].includes(c.choice)||(je(n,[l.id],`Prototype round: ${c.choice}. The board moved the card on.`),await y(L(e),n),c.choice==="no"))continue;const u=l.topic.slice(5);await La(e,u,"moving it to prototyping",async()=>{(await v(e,u)).item.state==="grilling"&&(await fe(e,u,{state:"prototyping"}),await J(e,u,{text:"You asked for a prototype round, so the board moved the card to prototyping."}),i.push(u))})}const o=new Map;for(const c of t){const l=a.get(String(c.id)),u=Ia(l?.topic);u&&o.set(u,[...o.get(u)||[],rs(l,c)])}const s=[];for(const[c,l]of o)await v(e,c).catch(()=>null)&&await La(e,c,"writing them into its notes",async()=>{await On(e,c,`Answers (owner, ${jn()})`,l.join(`
|
|
18
|
-
`))
|
|
19
|
-
`),await J(e,t,{text:`The card's notes file ${s.plan} was missing, so the board started a new one with your answers.`}));else{const u=String(s.id).toLowerCase().replace(/[^a-z0-9-]+/g,"-");let w=`${u}-notes.md`;for(let p=2;me($.join(i.dir,w));p++)w=`${u}-notes-${p}.md`;c=$.join(i.dir,w),await A.writeFile(c,`# ${s.title}
|
|
20
|
-
`,{flag:"wx"}),s.plan=w,await y(i.file,o)}const l=await A.readFile(c,"utf8");await A.writeFile(c,`${l.replace(/\n*$/,`
|
|
14
|
+
${yn}`,login:d});return await Ba(e,i,{state:"posted",text:o,review_url:p}),{lines:[`posted review on PR #${t}: ${p}`]}}catch(u){if(u instanceof re)throw u;l(u.message)}}r(xc,"postIncomingReview");const Wt=r(e=>m.join(e,"data","handshake.json"),"handshakeFile");async function La(e){const t=await $(Wt(e),null);if(!t?.hello)return t;const{hello:n,...a}=t;return{...a,ping:a.ping||n}}r(La,"readHandshake");async function vc(e,t){A(e),Pe(t,"the event id");const n=(await G(m.join(e,"data","events.jsonl"))).find(i=>i.id===t);n?.kind!=="ping"&&n?.kind!=="hello"&&l(`no ping event "${t}" in this site's data/events.jsonl`);const a=await La(e)||{};return a.ping?.id&&a.ping.id!==t&&l(`"${t}" is an older ping; ack the newest, "${a.ping.id}"`),await y(Wt(e),{...a,ping:a.ping||{id:t,at:n.ts,session:null},ack:{id:t,at:new Date().toISOString()}}),Wt(e)}r(vc,"ackPing");async function _c(e){const t=await La(e);t&&!t.done_at&&await y(Wt(e),{...t,done_at:new Date().toISOString()})}r(_c,"finishHandshake");const On={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})$/},gt=r((e,t)=>e.trim().toLowerCase()===t.trim().toLowerCase(),"sameName");function Pn(e,t){return Array.isArray(e)||l(`${t} must be a JSON array of reviewers`),e.forEach((n,a)=>{const i=`${t}[${a}]`;Ze(n,Object.keys(On),i);for(const[o,s]of Object.entries(On))(typeof n[o]!="string"||!s.test(n[o].trim()))&&l(`${i}.${o} must match ${s}`);e.slice(0,a).some(o=>gt(o.name,n.name))&&l(`${t} names "${n.name}" twice`)}),e.map(n=>Object.fromEntries(Object.keys(On).map(a=>[a,n[a].trim()])))}r(Pn,"checkReviewers");async function Dn(e,t){const{file:n,reviewers:a}=await kt(e,o=>{const s=Pn(t(o.reviewers||[]),"the reviewers");return{set:{reviewers:s},reviewers:s}}),i=[];for(const o of await F(e)){const s=await $(o.file,null);(s?.items||[]).some(c=>!Y(c))&&(await y(o.file,{...s,reviewers:a}),i.push(`wave ${o.slug}: reviewers copied`))}return{file:n,lines:i}}r(Dn,"writeReviewers");const Sc=r((e,t)=>Dn(e,()=>Pn(t,"the reviewer file")),"setReviewers"),Xe=r(e=>m.join(A(e),"data","pr-slack.json"),"prSlackFile"),Ut=r(e=>$(Xe(e),{}),"readPRSlack"),$t=r(e=>!!(e?.ts||e?.posting_at),"held");async function Fe(e,t){return wt(e,async()=>{const n=JSON.parse(await P.readFile(e,"utf8").catch(()=>"{}")),a=t(n);return await mt(e,JSON.stringify(n,null,2)+`
|
|
15
|
+
`),a})}r(Fe,"editJSON");async function Ht(e,t,n){await Fe(e,i=>{const o=t(i);$t(o)&&l("that Slack message has already gone out"),o.posting_at=new Date().toISOString()});let a;try{a=await n()}catch(i){throw await Fe(e,o=>{delete t(o).posting_at}),i}try{await Fe(e,i=>{const o=t(i);o.ts=a,delete o.posting_at})}catch(i){l(`posted, but its record failed, so it won't go again: ${i.message}`)}return a}r(Ht,"postOnce");const jc=r((e,t,n,a)=>Ht(Xe(e),i=>{const o=i[t.pr]||={head:t.head,links:{},replies:[]};return o.head??=t.head,Object.assign(o.links[t.reviewer]||={reactions:[]},{channel:n})},()=>a(n)),"postLink"),Oc=r((e,t,n)=>Ht(Xe(e),a=>a[t.pr].replies.find(i=>i.id===t.id).to[t.reviewer],n),"postReply"),Pc=r((e,t)=>Fe(Xe(e),n=>{const a=n[t.pr].links[t.reviewer];a.reactions.includes(t.emoji)||a.reactions.push(t.emoji)}),"recordReaction"),Ca=r((e,t,n,a)=>Fe(Xe(e),i=>{const o=i[t],s=Object.keys(o?.links||{}).filter(c=>o.links[c].ts);return s.length?(o.replies.push({id:`${n}-${o.replies.length+1}`,text:a,to:Object.fromEntries(s.map(c=>[c,{}]))}),s):[]}),"queueReply");function bs(e,t,n){const a=e.commits||[],i=a.findIndex(o=>o.oid===n);return[`New push to #${t}`,a.slice(i+1).map(o=>o.subject).join(" \xB7 ")].filter(Boolean).join(`
|
|
16
|
+
`)}r(bs,"pushText");async function Dc(e){const t=await $(mn,{}),n=r((a,i)=>{const o=X(t.prs,a)?.head;return!!o&&o!==i.head&&Object.values(i.links).some(s=>s.ts)},"moved");return Object.entries(await Ut(e)).some(([a,i])=>n(a,i))?Fe(Xe(e),a=>{const i=[];for(const[o,s]of Object.entries(a)){if(!n(o,s))continue;const c=X(t.prs,o),d=Object.keys(s.links).filter(u=>s.links[u].ts);s.head&&(s.replies.push({id:`push-${s.replies.length+1}`,text:bs(c,o.split("#")[1],s.head),to:Object.fromEntries(d.map(u=>[u,{}]))}),i.push(o)),s.head=c.head}return i}):[]}r(Dc,"queuePushes");function ys(e,t,n){const a=Number.isInteger(e.additions)?`+${e.additions}/-${e.deletions}, ${e.files} file${e.files===1?"":"s"}`:null;return[e.title||n.title,vn(t),a].filter(Boolean).join(`
|
|
17
|
+
`)}r(ys,"linkText");const Ta=r(e=>!!e?.approved||e?.reviewDecision==="APPROVED","approved");async function Ac(e,t){A(e);const n=await ma.waiting("slack");if(n)return{waiting:n,links:[],replies:[],reactions:[],digest:null};const a=await ie(e),i=await $(mn,{}),o=await Ut(e),s=[];for(const p of await F(e)){const w=await $(p.file,{}),f=_n(w,a.reviewers);for(const h of w.items||[])if(h.lane!=="done")for(const g of Le(h,oe(e,w))){const k=ht(g),v=X(i.prs,k);if(!(v?.state!=="OPEN"||v.isDraft))for(const D of f)$t(o[k]?.links?.[D.name])||s.some(O=>O.pr===k&&O.reviewer===D.name)||s.push({pr:k,key:`${p.slug}/${h.id}`,reviewer:D.name,slack_user:D.slack_user,head:v.head||null,text:ys(v,g,h)})}}const c=[],d=[];for(const[p,w]of Object.entries(o)){const f=X(i.prs,p),h=[Ta(f)&&"git_approved",f?.state==="MERGED"?"git_merged":f?.state==="CLOSED"&&"x"].filter(Boolean);for(const[g,k]of Object.entries(w.links))if(k.ts)for(const v of h)k.reactions.includes(v)||d.push({pr:p,reviewer:g,channel:k.channel,ts:k.ts,emoji:v});for(const g of w.replies)for(const[k,v]of Object.entries(g.to))$t(v)||c.push({pr:p,id:g.id,reviewer:k,channel:w.links[k].channel,thread_ts:w.links[k].ts,text:g.text})}const u=await xs(e,t);return{links:s,replies:c,reactions:d,digest:u.due?u:null}}r(Ac,"reviewDue");async function Rc(e,t,n){A(e);const a=S(n,"what changed"),{wave:i,item:o}=await x(e,t),s=[];for(const d of Le(o,oe(e,i)))s.push(...await Ca(e,ht(d),"updated",`Updated: ${a}`));const c=[...new Set(s)];return{lines:[c.length?`Updated queued for ${c.join(", ")} on ${t}`:`no reviewer has a link to ${t}'s PRs; nothing queued`]}}r(Rc,"reviewUpdated");async function Nc(e,t){const{wave:n,item:a}=await x(e,t),i=await Ut(e);return Le(a,oe(e,n)).some(o=>Object.values(i[ht(o)]?.links||{}).some(s=>s.ts))}r(Nc,"cardLinked");const qa=r(e=>m.join(A(e),"data","review-digests.json"),"digestFile");async function xs(e,{now:t=new Date}={}){A(e);const n=await ie(e),a=await $(mn,{}),i=await Ut(e),o=Fo(),s=new Map;for(const f of await F(e)){const h=await $(f.file,{}),g=_n(h,n.reviewers);for(const k of g)s.has(k.name)||s.set(k.name,{name:k.name,slack_user:k.slack_user,dm:k.dm,items:[]});for(const k of h.items||[]){if(k.lane==="done")continue;const v=`${f.slug}/${k.id}`,D=qo(h,k,v,o);for(const O of Le(k,oe(e,h))){const B=ht(O),N=X(a.prs,B);if(!(N?.state!=="OPEN"||N.isDraft||N.failing?.length||Ta(N)))for(const _ of g){const E=i[B]?.links?.[_.name]?.ts||X(D,_.name)?.ts||null,C=E?(t.getTime()-Number(E)*1e3)/864e5:null,T=C>2?Math.floor(C):null;s.get(_.name).items.push({key:v,pr:B,title:k.title,url:vn(O),sent_ts:E,...T?{days_old:T}:{},text:`${vn(O)} \xB7 ${k.title}${T?` (${T} days old)`:""}`})}}}}const{date:c,open:d}=Mo(t),u=((await $(qa(e),{})).digests||[]).filter(f=>f.date===c),p=await ma.waiting("slack");for(const f of u)s.has(f.reviewer)||s.set(f.reviewer,{name:f.reviewer,items:[]});const w=[...s.values()].map(f=>{const h=u.find(k=>k.reviewer===f.name),g=h?!h.posting_at&&(!h.ts||h.replies.some(k=>!$t(k))):f.items.length>0;return{...f,owed:g,...h?{sent:h}:{}}}).filter(f=>f.items.length||f.sent);return{date:c,due:!p&&d&&w.some(f=>f.owed),...p?{waiting:p}:{},reviewers:w}}r(xs,"reviewDigest");async function Ec(e,t,n,{channel:a,header:i,post:o}){const s=qa(e),c=r(p=>(p.digests||[]).find(w=>w.date===t&&w.reviewer===n.name),"entry");await Fe(s,p=>{c(p)||(p.digests=[...(p.digests||[]).filter(w=>w.date===t),{date:t,reviewer:n.name,channel:a,replies:n.items.map(w=>({pr:w.pr,text:w.text}))}],delete p.last_date)});const d=c(await $(s,{})),u=d.ts||await Ht(s,c,()=>o({channel:d.channel,text:i(d.replies.length)}));for(const[p,w]of d.replies.entries())$t(w)||await Ht(s,f=>c(f).replies[p],()=>o({channel:d.channel,thread_ts:u,text:w.text}))}r(Ec,"postDigest");function Ic(e,t){const n=Pn(Array.isArray(t)?t:[t],"the reviewer file");return Dn(e,a=>[...a.map(i=>n.find(o=>gt(o.name,i.name))||i),...n.filter(i=>!a.some(o=>gt(i.name,o.name)))])}r(Ic,"addReviewers");function Bc(e,t){return Dn(e,n=>{const a=t.filter(i=>!n.some(o=>gt(o.name,i)));return a.length&&l(`this board has no reviewer ${a.map(i=>`"${i}"`).join(", ")}`),n.filter(i=>!t.some(o=>gt(i.name,o)))})}r(Bc,"removeReviewers");const vs=r(()=>m.join(ln.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),An=r(e=>{try{return e()}catch(t){throw t instanceof zo&&l(t.message),t}},"ruleCall"),Rn=r(async e=>$a(await $(m.join(e,"board.json"),{})),"readBoardRules");let Fa=Promise.resolve();function kt(e,t){const n=Fa.then(async()=>{const a=m.join(A(e),"board.json"),i=await $(a,{}),{set:o,...s}=t(i);return await y(a,{...i,...o}),{file:a,...s}});return Fa=n.catch(()=>{}),n}r(kt,"writeBoardJSON");const Ma=r((e,t)=>kt(e,n=>{const a=An(()=>t($a(n)));return{set:{rules:a.rules},...a}}),"writeBoardRules");function Lc(e,t){const n=Object.keys(t||{}).sort().join(),a=n==="level,login",i=a?t.login:t?.[n],o=typeof i=="string"?i.trim():null;(!["add","remove","level,login"].includes(n)||!Vo(o))&&l("Send add or remove with one login, like name@github, or a listed login with its level."),a&&!["view","full"].includes(t.level)&&l("A level is view or full.");const s=r(({login:c,level:d})=>d==="full"?{login:c,level:d}:c,"stored");return kt(e,c=>{const d=Zo(c);a&&!d.some(w=>w.login===o)&&l(`${o} isn't shared with yet. Add them first.`);const p=(n==="remove"?d.filter(w=>w.login!==o):a?d.map(w=>w.login===o?{login:o,level:t.level}:w):d.some(w=>w.login===o)?d:[...d,{login:o,level:"view"}]).map(s);return{set:{share:p},share:p}}).then(({share:c})=>({share:c}))}r(Lc,"saveBoardShare");const Wa=r(e=>typeof e=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e),"validGithubLogin"),Ua=r(e=>{try{return e?kn(e):null}catch{return null}},"storedTickets"),Nn=r(e=>Number.isInteger(e)&&e>=1&&e<=99,"validCap"),En=["max_implementers","max_planners"],be=r((e,t)=>{const n=Ua(e?.tickets);return{auto_merge:e?.auto_merge===!0,auto_build:e?.auto_build===!0,auto_pull:e?.auto_pull===!0,max_implementers:Nn(e?.max_implementers)?e.max_implementers:ao,max_planners:Nn(e?.max_planners)?e.max_planners:io,auto_triage:e?.auto_triage===!0,auto_review:e?.auto_review===!0,idle_notification:e?.idle_notification===!0,skip_draft:e?.skip_draft===!0,prs:t?.hub?e?.prs===!0:e?.prs!==!1,...n?{tickets:n}:{},...Wa(e?.github_login)?{github_login:e.github_login}:{},...ga(e?.build_target)?{build_target:e.build_target}:{}}},"settingsOf");async function Cc(e,t,n=new Date){let a;try{a=go(t,n)}catch(i){l(i.message)}return await kt(e,()=>({set:{paused_until:a??void 0}})),a}r(Cc,"setBoardPause");async function _s(e){const t=A(e);return be(await $(m.join(t,"board.json"),{}),ge().find(n=>je(n.dir,t)))}r(_s,"readBoardSettings");const Tc=r((e,t)=>!!e?.github&&(!e.local||e.hub===!0)&&t.prs,"opensPRs");async function et(e,t){Ze(t,["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","tickets","github_login","build_target"],"the settings"),Object.keys(t).length||l("the settings name nothing to change"),t={...t};for(const[n,a]of Object.entries(t))if(n==="tickets"){try{t.tickets=kn(a)}catch(o){l(o.message)}t.tickets.source==="linear"&&!Po()&&l(jo);const i=t.tickets.source==="github"&&e?ge().find(o=>je(o.dir,e)):null;i&&!i.github&&l("GitHub issues need the board\u2019s repo on GitHub, and this board\u2019s repo isn\u2019t; pick internal or linear")}else n==="github_login"?a===""||a===null?t.github_login=void 0:Wa(a)||l("github_login must be a GitHub login"):n==="build_target"?a===null?t.build_target=void 0:ga(a)||l('build_target must name .kickflip/build-<target>: letters, digits, ".", "_" or "-", or null for .kickflip/build'):En.includes(n)?Nn(a)||l(`${n} must be a whole number from 1 to 99`):Tt(a,n);if(t.prs===!1){const n=e?ge().find(a=>je(a.dir,e)):null;n?.github&&!n.local&&!(await Wo(n.owner?.repo)).pushMain&&l("Merge lands each card on main and pushes it, so turn on Push main after a land first")}return kt(e,n=>{const a=e?ge().find(s=>je(s.dir,e)):null,i=be(n,a),o=be({...n,...t},a);return{set:t,settings:o,changed:Object.keys(t).filter(s=>JSON.stringify(i[s])!==JSON.stringify(o[s]))}})}r(et,"saveBoardSettings");async function qc(){if(Oo().set)return[];const e=[];for(const t of ge()){if((await _s(t.dir)).tickets?.source!=="linear")continue;const{settings:n}=await et(t.dir,{tickets:{source:"internal"}});await me(t,"settings",{tickets:n.tickets}),e.push(`/${t.route} fell back to internal tickets: Hub has no Linear key`)}return e}r(qc,"fallBackFromLinear");async function Fc(e,t,n){const a=String(t).replace(/-/g,"_"),i=["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","github_login","tickets","build_target"];if(i.includes(a)||l(`no board setting "${t}"; settings are ${i.map(c=>c.replace(/_/g,"-")).join(", ")}`),a==="tickets"){let c;try{c=JSON.parse(n)}catch{l('settings set tickets takes the source as JSON, such as {"source":"github"}')}const{file:d,settings:u}=await et(e,{tickets:c});return{file:d,lines:Ss({tickets:u.tickets})}}if(a==="github_login"){const{file:c,settings:d}=await et(e,{github_login:n});return{file:c,lines:[`github-login ${d.github_login||"unset"}`]}}if(a==="build_target"){const{file:c,settings:d}=await et(e,{build_target:n==="default"?null:n});return{file:c,lines:[`build-target ${d.build_target||"default"}`]}}if(En.includes(a)){const{file:c,settings:d}=await et(e,{[a]:/^\d+$/.test(n)?Number(n):n});return{file:c,lines:[`${t} ${d[a]}`]}}["on","off"].includes(n)||l(`settings set ${t} takes on or off, not "${n}"`);const{file:o,settings:s}=await et(e,{[a]:n==="on"});return{file:o,lines:[`${t} ${s[a]?"on":"off"}`]}}r(Fc,"setBoardSetting");const Ss=r(e=>Object.entries(e).map(([t,n])=>`setting ${t.replace(/_/g,"-")} ${t==="tickets"?JSON.stringify(n):t==="github_login"||En.includes(t)?n:t==="build_target"?n||"default":n?"on":"off"}`),"settingsLines");async function In(e,t,n){const a=t?An(()=>n(Lt())):await Ma(e,n);return t&&An(()=>Yo(a.rules)),{file:t?vs():a.file,lines:[`id ${a.rule.id}`]}}r(In,"changeRules");async function Ha(e,t,n){const a=r(s=>s.map(c=>c.id),"ids");n||A(e);const[i,o]=n?[a(Lt()),e?a(await Rn(e)):[]]:[a(await Rn(e)),a(Lt())];i.includes(t)||l(`no ${n?"hub":"board"} rule "${t}"${o.includes(t)?`; it is a ${n?"board":"hub"} rule, so ${n?"drop":"pass"} --hub`:""}`)}r(Ha,"checkRuleId");async function Mc(e){const t=r(n=>a=>`${n} ${a.id} ${a.by==="claude"?"claude":"owner"} ${a.text}`,"line");return{lines:[...Lt().map(t("hub")),...e?(await Rn(e)).map(t("board")):[]]}}r(Mc,"listRules");function Wc(e,{text:t,hub:n=!1,by:a}){return n||A(e),In(e,n,i=>Ko(i,{text:t,by:a}))}r(Wc,"addRuleTo");async function Uc(e,t,{text:n,hub:a=!1}){return await Ha(e,t,a),In(e,a,i=>Jo(i,t,n))}r(Uc,"editRuleIn");async function Hc(e,t,{hub:n=!1}){return await Ha(e,t,n),In(e,n,a=>Qo(a,t))}r(Hc,"removeRuleFrom");const zc=r((e,t,n)=>Ma(e,a=>Go(a,t,n)),"saveBoardRules");async function Kc(e,{id:t,title:n,summary:a}){A(e);const i=await $(L(e),{topics:[],items:[]}),o=i.topics||[];return o.some(s=>s.id===S(t,"the topic id"))&&l(`topic "${t}" already exists`),o.push({id:t.trim(),title:S(n,"--title"),...a===void 0?{}:{summary:S(a,"--summary")}}),await y(L(e),{...i,topics:o,items:i.items||[]}),L(e)}r(Kc,"addTopic");const za=["quiet","info","notice","danger","success"];function js(e,t){if(e.diagram===void 0!=(e.practice===void 0)&&l(`${t} needs diagram and practice together`),e.diagram===void 0)return;const n=e.diagram;(!Array.isArray(n)||!n.length)&&l(`${t}.diagram must be a non-empty array of rows`),n.forEach((a,i)=>{(!Array.isArray(a)||!a.length||a.length>4)&&l(`${t}.diagram[${i}] must be a row of 1 to 4 boxes`),a.forEach((o,s)=>{const c=`${t}.diagram[${i}][${s}]`;Ze(o,["text","tone","to"],c),S(o.text,`${c}.text`),ne(o.tone,d=>za.includes(d)?d:l(`${c}.tone takes ${za.join(", ")}`)),ne(o.to,d=>Array.isArray(d)&&i+1<n.length&&d.every(u=>Number.isInteger(u)&&u>=0&&u<n[i+1].length)?d:l(`${c}.to must list boxes of the next row by index`))})}),(!Array.isArray(e.practice)||!e.practice.length)&&l(`${t}.practice must be a non-empty array of lines`),e.practice.forEach((a,i)=>S(a,`${t}.practice[${i}]`))}r(js,"checkDiagram");function Os(e,t){(!Array.isArray(e)||!e.length)&&l(`${t}.options must be a non-empty array`);const n=new Set;e.forEach((a,i)=>{const o=`${t}.options[${i}]`;Ze(a,["id","label","recommended","detail","diagram","practice"],o),js(a,o),n.has(S(a.id,`${o}.id`))&&l(`${t} has two options with id "${a.id}"`),n.add(a.id),S(a.label,`${o}.label`).startsWith(`${a.id}. `)&&l(`${o}.label starts with "${a.id}. ", but the page adds the option letter itself`),ne(a.recommended,s=>Tt(s,`${o}.recommended`)),ne(a.detail,s=>typeof s=="string"?s:l(`${o}.detail must be text`))}),e.filter(a=>a.recommended).length>1&&l(`${t} recommends more than one option`)}r(Os,"checkOptions");function Ps(e,t,n){Ze(e,["option","card","answer"],n),t.some(a=>a.id===S(e.option,`${n}.option`))||l(`${n}.option names no option "${e.option}"`),S(e.card,`${n}.card`),S(e.answer,`${n}.answer`)}r(Ps,"checkSuggested");const tt=r(e=>m.join(e,"data","prototypes.json"),"prototypesFile"),Bn=r((e,t,n)=>m.join(e,"public","prototypes",t,`${n}.js`),"takeFile");async function bt(e){const t=e?(await $(tt(e),{})).prototypes:null;return Array.isArray(t)?t:[]}r(bt,"readPrototypes");const Ds=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:l(`a prototype topic uses only a-z, 0-9 and "-", not "${e}"`),"prototypeTopic");function As(e){const t=/^([^:]*):(.*)$/s.exec(e);(!t||!/^[A-Z]$/.test(t[1]))&&l(`--take takes KEY:"Label" with KEY one capital letter, not "${e}"`);const n=S(t[2],`--take ${t[1]}'s label`);return/^[A-Z]\.\s/.test(n)&&l(`--take ${t[1]}'s label starts with "${n.slice(0,3)}"; the take pill shows the label alone, so drop the letter`),{key:t[1],label:n}}r(As,"prototypeTake");function Rs(e){const t=/^([^:]*):(.*)$/s.exec(e);return(!t||!/^[a-z0-9][a-z0-9-]*$/.test(t[1]))&&l(`--state takes KEY:"Label" with KEY in a-z, 0-9 and "-", not "${e}"`),{key:t[1],label:S(t[2],`--state ${t[1]}'s label`)}}r(Rs,"prototypeState");const Ka=r(e=>e.find((t,n)=>e.findIndex(a=>a.key===t.key)!==n),"twiceIn"),Ae=r(e=>/^prototype-(.+)\.html$/.exec(m.posix.basename(String(e||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function Jc(e,{topic:t,title:n,card:a,takes:i=[],served:o,states:s=[]}){A(e),Ds(t),S(n,"--title"),i.length||l('prototype add needs a --take KEY:"Label" for each take');const c=i.map(As),d=Ka(c);d&&l(`--take ${d.key} is given twice`),o!==void 0&&!/^\/\S*$/.test(o)&&l(`--served takes the page's path on the served site, starting with "/", not "${o}"`),s.length&&o===void 0&&l("--state goes with --served: only a served round re-renders its states in place");const u=s.map(Rs),p=Ka(u);p&&l(`--state ${p.key} is given twice`),a!==void 0&&await x(e,a);const w=await bt(e);w.some(g=>g.topic===t)&&l(`prototype ${t} is already recorded; run prototype rm ${t} first, or pick another topic`),await P.stat(m.join(e,"public",`prototype-${t}.html`)).then(()=>!0,()=>!1)&&l(`public/prototype-${t}.html is a hand-written page that questions may link; pick another topic`);const f={topic:t,title:n.trim(),...a===void 0?{}:{card:a},...o===void 0?{}:{served:o},takes:c,...u.length?{states:u}:{},at:new Date().toISOString()};await y(tt(e),{prototypes:[...w,f]});const h=`page prototype-${t}.html`;return o!==void 0?{file:tt(e),lines:[h,`served ${o} from the base prototype worktree (board.mjs prototype base)`,...u.length?[`states ${u.map(g=>g.key).join(", ")}`]:[]]}:{file:tt(e),lines:[h,...c.map(g=>`take ${g.key} ${Bn(e,t,g.key)}`)]}}r(Jc,"addPrototype");async function Qc(e){A(e);const t=await bt(e);return{lines:t.length?t.map(n=>[n.topic,n.title,n.takes.map(a=>`${a.key}: ${a.label}`).join(", "),n.card,n.served&&`served ${n.served}`,n.states&&`states ${n.states.map(a=>a.key).join(", ")}`].filter(Boolean).join(" \xB7 ")):["no prototypes recorded"]}}r(Qc,"listPrototypes");async function Gc(e){const t=(await bt(e)).filter(a=>a.served).map(a=>a.topic);if(!t.length)return[];const n=await $(L(e),{items:[]});return t.filter(a=>(n.items||[]).some(i=>!i.resolved&&Ae(i.url)===a))}r(Gc,"servedRoundsOpen");async function Yc(e,t){A(e);const n=await bt(e);n.some(o=>o.topic===t)||l(`no prototype "${t}" is recorded`);const i=((await $(L(e),{items:[]})).items||[]).filter(o=>!o.resolved&&Ae(o.url)===t).map(o=>o.id);return i.length&&l(`prototype ${t} backs open question ${i.join(", ")}; resolve ${i.length===1?"it":"them"} first`),await y(tt(e),{prototypes:n.filter(o=>o.topic!==t)}),tt(e)}r(Yc,"removePrototype");async function Ns(e,t,n){const a=t.url?.split(/[?#]/)[0];if(!a||!m.posix.basename(a).startsWith("prototype-"))return;const i=m.posix.basename(a),o=t.url.split("#")[0].split("?").slice(1).join("?"),s=`${n}.url`;new URLSearchParams(o).get("q")!==t.id&&l(`${s} must be ${i}?q=${t.id} so the pill can pick`);const c=Ae(a),d=(await bt(e)).find(f=>f.topic===c);d||l(`${s} names ${i}, which is not a recorded prototype; record it with board.mjs prototype add ${c} --title "\u2026" --take A:"\u2026", then write its take files`);const u=d.takes.map(f=>f.key),p=t.options.map(f=>f.id),w=p.map(f=>u.indexOf(f));if(w.some((f,h)=>f<0||h>0&&f<w[h-1])&&l(`${n}.options ids ${p.join(", ")} must be a non-empty subset of ${i}'s takes, in take order: ${u.join(", ")}`),!d.served)for(const f of u)await P.stat(Bn(e,c,f)).then(h=>h.isFile(),()=>!1)||l(`${s} take ${f} has no file yet; write ${Bn(e,c,f)}`)}r(Ns,"checkPrototype");async function Zc(e,t,n=async()=>{}){A(e);const a=Array.isArray(t)?t:[t];a.length||l("the question file is empty");const i=await $(L(e),{topics:[],items:[]}),o=i.items||[],s=await $($e(e),{}),c=new Set([...o,...Object.values(s).flat()].map(w=>w.id)),d=new Set((i.topics||[]).map(w=>w.id));for(const[w,f]of a.entries()){const h=`question ${f?.id??w+1}`;Ze(f,["id","topic","source","question","context","url","urgent","options","round_of","reask_of","ask","suggested","prototype_gate"],h),c.has(Pe(f.id,`${h}.id`))&&l(`question id "${f.id}" is already used`),c.add(f.id),S(f.topic,`${h}.topic`),f.topic.startsWith("item:ask:")?await ot(e,f.topic.slice(9),`${h}.topic`):f.topic.startsWith("item:")?await x(e,f.topic.slice(5)):f.topic.startsWith("lineup:")?await tn(e,f.topic.slice(7)):f.topic.startsWith("wave:")?await pr(e,f.topic.slice(5)):d.has(f.topic)||l(`topic "${f.topic}" is not declared; add it with: topic add ${f.topic} --title <t>`),S(f.question,`${h}.question`);for(const g of["source","context"])ne(f[g],k=>S(k,`${h}.${g}`));ne(f.url,g=>ba(g,`${h}.url`)),ne(f.urgent,g=>Tt(g,`${h}.urgent`)),Os(f.options,h),ne(f.prototype_gate,g=>Es(g,f,h)),ne(f.suggested,g=>Ps(g,f.options,`${h}.suggested`)),await Ns(e,f,h),f.ask!==void 0&&await jr(e,f,h),ne(f.round_of,g=>{const k=o.find(v=>v.id===g);k||l(`${h}.round_of names no question "${g}" in owner.json`),k.topic!==f.topic&&l(`${h}.round_of names ${g} on topic ${k.topic}; a round stays on its question's topic, ${f.topic}`),Ae(f.url)||l(`${h}.round_of needs a url on a recorded prototype, prototype-<topic>.html?q=${f.id}`)}),ne(f.reask_of,g=>{const k=o.find(v=>v.id===g);k||l(`${h}.reask_of names no question "${g}" in owner.json`),k.topic!==f.topic&&l(`${h}.reask_of names ${g} on topic ${k.topic}; a re-ask stays on its question's topic, ${f.topic}`),k.resolved||l(`${h}.reask_of names ${g}, which is not resolved; only a resolved question is asked again`)})}for(const w of new Set(a.map(f=>Ae(f.url)).filter(Boolean)))await n(e,w);const u=[],p=[];for(const w of a){const f=w.topic.startsWith("item:")&&!w.topic.startsWith("item:ask:")?w.topic.slice(5):null,h=f&&await Ja(e,f);h&&((s[f]||=[]).push(w),p.push(w),u.push(`staged: ${w.id} waits for ${f}'s ${h} to finish; the card's staged questions then publish together`))}return p.length&&await y($e(e),s),p.length===a.length?{file:$e(e),lines:u,staged:p.length}:Ga(e,i,o,a.filter(w=>!p.includes(w)),u)}r(Zc,"addQuestions");async function Ja(e,t){const{source:n}=await x(e,t);for(const a of await H(n.dir,I(t).id)){const i=await $(a,null);if(i?.started&&!i.done&&Be(i)&&i.kind!==we)return i.kind}return null}r(Ja,"runningPlanner");async function nt(e,t,{screen:n=!1}={}){if(!e||await Ja(e,t))return;const a=await $($e(e),{});if(!a[t])return;const i=await $(Ye(e),{});if(n){i[t]||await y(Ye(e),{...i,[t]:new Date().toISOString()});return}const{[t]:o,...s}=a,c=await $(L(e),{topics:[],items:[]});await Ga(e,c,c.items||[],o,[]),await y($e(e),s),i[t]&&await y(Ye(e),Ln(i,t))}r(nt,"publishStaged");const Ln=r((e,t)=>Object.fromEntries(Object.entries(e).filter(([n])=>n!==t)),"withoutKey");async function Qa(e){const t=await $(Ye(e),{}),n=await $($e(e),{});return Object.fromEntries(Object.entries(t).filter(([a])=>n[a]).map(([a,i])=>[a,{since:i,ids:n[a].map(o=>o.id)}]))}r(Qa,"readScreening");async function Vc(e,t,n){A(e);const a=S(n,"--note"),i=await $($e(e),{}),o=await $(Ye(e),{}),s=Object.keys(i).find(w=>i[w].some(f=>f.id===t));s||l(`no staged question "${t}"; question settle takes a question a finished grill left for you to screen`),o[s]||l(`${t} waits for ${s}'s planner to finish`);const c=i[s].find(w=>w.id===t);c.prototype_gate&&l(`${t} is the card's prototype question, which is the owner's; question publish ${s} passes it on`);const d=new Date().toISOString(),u=await $(L(e),{topics:[],items:[]});await y(L(e),{...u,topics:u.topics||[],items:[...u.items||[],{...c,asked_at:d,resolved:`Settled by the orchestrator: ${a}`,resolved_at:d}]});const p=i[s].filter(w=>w.id!==t);return await y($e(e),p.length?{...i,[s]:p}:Ln(i,s)),p.length||await y(Ye(e),Ln(o,s)),await at(e,s,`Answers (orchestrator, ${yt()})`,`- ${t} (${c.question}): settled by the orchestrator. "${a}"`),L(e)}r(Vc,"settleQuestion");async function Xc(e,t){A(e);const n=(await Qa(e))[t]?.ids||l(`${t} has no questions waiting on you to screen`);return await nt(e,t),n}r(Xc,"publishScreened");async function ed(e,t=new Date){const n=Object.entries(await Qa(e)).filter(([,a])=>t-Date.parse(a.since)>=es).map(([a])=>a);for(const a of n)await nt(e,a),await U(e,a,{text:"The orchestrator didn't screen the grill's questions within 15 minutes, so the board passed them to you as they are."});return n}r(ed,"publishUnscreened");async function Ga(e,t,n,a,i){const{answers:o,requests:s,reopens:c}=await it(e),d=new Date().toISOString(),u=[],p=[];for(const f of a.filter(h=>h.round_of))f.round=hn(n.find(h=>h.id===f.round_of))+1,Re(t,[f.round_of],`Followed by prototype round ${f.round}`,d),p.push(f.round_of);for(const f of a.filter(h=>h.reask_of))Re(t,[f.reask_of],`Asked again as ${f.id}`,d),p.push(f.reask_of);for(const f of a){const h=Ae(f.url),g=n.filter(v=>v.topic===f.topic).map(v=>Ie(v,o,s,[],c)),k=f.topic.startsWith("item:")?ni(g):[];for(const v of g)k.includes(v.id)?(Re(t,[v.id],`Followed by ${f.id}`,d),p.push(v.id)):!v.resolved&&h&&Ae(v.url)===h&&(Re(t,[v.id],`Replaced by ${f.id}`,d),u.push(v.id))}const w={...t,topics:t.topics||[],items:[...n,...a.map(f=>({...f,asked_at:d}))]};await y(L(e),w);for(const f of new Set(a.map(h=>Cn(h.topic)).filter(Boolean)))await Ms(e,f);return{file:L(e),lines:[...i,...u.map(f=>`replaced: ${f}`),...p.map(f=>`resolved: ${f}`)]}}r(Ga,"publishQuestions");function Es(e,t,n){Tt(e,`${n}.prototype_gate`),e&&((!t.topic.startsWith("item:")||t.topic.startsWith("item:ask:"))&&l(`${n}.prototype_gate goes on a card's topic, item:<wave>/<id>`),t.options?.map(a=>a.id).join()!=="yes,no"&&l(`${n}.prototype_gate takes exactly the options yes and no, in that order`))}r(Es,"checkGate");async function td(e,t){const n=await $(L(e),{topics:[],items:[]}),a=new Map((n.items||[]).map(c=>[c.id,c]));for(const c of t){const d=a.get(String(c.id));if(!d?.prototype_gate||!["yes","no"].includes(c.choice))continue;Re(n,[d.id],`Prototype round: ${c.choice}. The board moves the card on once every question is answered.`),await y(L(e),n);const u=d.topic.slice(5);c.choice==="yes"&&(await x(e,u).catch(()=>null))?.item.phase==="grilling"&&await Un(e,u)&&await U(e,u,{text:"You asked for a prototype round, so the board moves the card to prototyping once every question is answered."})}const i=new Map;for(const c of t){const d=a.get(String(c.id)),u=Cn(d?.topic);u&&i.set(u,[...i.get(u)||[],Bs(d,c)])}const o=[],s=[];for(const[c,d]of i)await x(e,c).catch(()=>null)&&await Is(e,c,"writing them into its notes",async()=>{await at(e,c,`Answers (owner, ${yt()})`,d.join(`
|
|
18
|
+
`));const u=await Ya(e,c);u==="prototyping"&&o.push(c),u==="staging"&&s.push(c)});return{prototype:o,staged:s}}r(td,"settleAnswers");async function Ya(e,t,{roundAt:n=!1}={}){const{item:a}=await x(e,t),{asked:i}=await de(e,t);if(i.some(o=>o.state==="open"))return null;if(a.phase==="grilling"&&i.some(o=>o.prototype_gate&&o.answers.at(-1)?.choice==="yes")){if(await pe(e,t,{phase:"prototyping"}),n){const{source:o,wave:s,item:c}=await x(e,t);c.round_at=new Date().toISOString(),await y(o.file,s)}return await U(e,t,{text:"You asked for a prototype round, so the board moved the card to prototyping."}),"prototyping"}return await qs(e,t)?(await Wn(e,t),"staging"):null}r(Ya,"moveOnAnswered");async function Is(e,t,n,a){try{await a()}catch(i){if(!(i instanceof re))throw i;await U(e,t,{text:`Your answers were recorded, but ${n} failed: ${i.message}`})}}r(Is,"followUp");const Cn=r(e=>typeof e=="string"&&e.startsWith("item:")&&!e.startsWith("item:ask:")?e.slice(5):null,"cardOfTopic"),zt=r(e=>String(e).padStart(2,"0"),"pad2"),yt=r((e=new Date)=>`${zt(e.getMonth()+1)}/${zt(e.getDate())} ${zt(e.getHours())}:${zt(e.getMinutes())}`,"noteStamp");function Bs(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(Bs,"answerLine");const Za=292,Tn=r(e=>String(e).toLowerCase().replace(/[^a-z0-9-]+/g,"-"),"notesBase"),Ls=r((e,t,n)=>wt(m.join(e,`.${Tn(t)}-notes`),n),"notesLock");function qn(e,t){let n=`${Tn(t)}-notes.md`;for(let a=2;fe(m.join(e,n));a++)n=`${Tn(t)}-notes-${a}.md`;return n}r(qn,"freeNotesName");async function Fn(e,t){const n=m.join(m.dirname(e),`.${m.basename(e)}.${process.pid}.tmp`);await P.rm(n,{force:!0}),await P.writeFile(n,t,{mode:Za}),await P.rename(n,e)}r(Fn,"writeNotesFile");const Cs=r(e=>/\n/.test(S(e,"--heading"))?l("--heading is one line, the section's title"):S(e,"--heading"),"notesHeading"),Ts=r((e,t,n)=>`${e.replace(/\n*$/,`
|
|
21
19
|
`)}
|
|
22
|
-
## ${
|
|
23
|
-
${
|
|
24
|
-
|
|
25
|
-
`),"messageWords");async function vc(e,t,n){const{item:a}=await v(e,t);a.held&&await Va(e,"card-hold",{key:t,hold:!1});const{owner:i,asked:o}=await ue(e,t),s=o.filter(l=>l.state==="open").map(l=>l.id);s.length&&await Tt(e,i,s,"Skipped: you tapped Implement");const c=Ba(n);c&&await On(e,t,`Implement (owner, ${jn()}), verbatim`,c),await Pn(e,t,"implement")}r(vc,"stageImplement");async function _c(e,t,n){const a=Ba(n);a&&await On(e,t,`Reply (owner, ${jn()}), verbatim`,a),await Pn(e,t,"reply")}r(_c,"stageReply");async function Sc(e,t,n){const{source:a,wave:i,item:o}=await v(e,t);!["ready","staging"].includes(o.state)||Je(o)||(o.state=n==="prototype"?"prototyping":"grilling",delete o.staged_at,await y(a.file,i))}r(Sc,"planAgain");async function ds(e,t){const n=await v(e,t).catch(()=>null);!n||!["ready","staging"].includes(n.item.state)||Je(n.item)||(n.item.state="grilling",delete n.item.staged_at,await y(n.source.file,n.wave),await J(e,t,{text:"A new question on it, so it is grilling until you answer."}))}r(ds,"grillAgain");function Ca(e){return e.filter(t=>t.state==="answered").map(t=>t.id)}r(Ca,"answeredOnClaude");function je(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(je,"setResolved");async function Tt(e,t,n,a){return je(t,n,a),await y(L(e),t),L(e)}r(Tt,"markResolved");async function jc(e,t,n){P(e);const a=await g(L(e),{topics:[],items:[]}),i=(a.items||[]).find(p=>p.id===t);i||d(`no question "${t}" in owner.json`);const{answers:o,requests:s,reopens:c}=await Ke(e),l=Pe(i,o,s,[],c);if(l.state==="resolved")return L(e);l.state==="requested"&&d(`${t} waits on the prototype the owner asked for; question add with round_of ${t} resolves it`);const u=j(n,"--note");Se(i.url)&&us(a,i,o);const w=await Tt(e,a,[t],u);return await Wa(e,i.topic),w}r(jc,"resolveQuestion");async function Oc(e,t,n,a=[]){P(e);const[i,...o]=[...new Set(t)];i||d("question move takes the ids of the questions to merge"),await v(e,n);const s=[n,...a];new Set(s).size!==s.length&&d("question move names a card twice");for(const f of a)await v(e,f);const c=await g(L(e),{topics:[],items:[]}),{answers:l,requests:u,reopens:w}=await Ke(e);for(const f of[i,...o]){const m=(c.items||[]).find(k=>k.id===f)||d(`no question "${f}" in owner.json`),{state:h}=Pe(m,l,u,[],w);h!=="open"&&d(`${f} is ${h}; question move takes open questions`)}for(const[f,m]of a.entries()){const{item:h}=await v(e,m),k=[...h.deps||[],s[f]].join(",");await fe(e,m,{deps:k,relate:n})}const p=await X(e,n);return Object.assign(c.items.find(f=>f.id===i),{topic:`item:${n}`}),je(c,o,`Duplicate of ${p}'s question ${i}`),await y(L(e),c),L(e)}r(Oc,"moveQuestions");async function Pc(e,t){const n=t.filter(({choice:i,note:o})=>!i&&String(o||"").trim()).map(i=>i.id),a=n.length?await g(L(e),{topics:[],items:[]}):null;a?.items?.some(i=>n.includes(i.id))&&(je(a,n,"Answered with a note"),await y(L(e),a))}r(Pc,"resolveNoteAnswers");function us(e,t,n){const a=an(t),i=e.items.filter(o=>o.topic===t.topic&&an(o)===a).map(o=>Pe(o,n));qi(i,a)||d(`${t.id} is in prototype round ${a}, which no pick has settled yet; a pick settles it`)}r(us,"roundSettled");async function Ke(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=>{const i=await Promise.all(["html","json"].map(o=>A.stat($.join(e,"data","replies",`${a.id}.${o}`)).then(s=>s.isFile(),()=>!1)));return{...a,reply:a.reply||i.some(Boolean)}})),requests:t.filter(a=>a.kind==="prototype-request"),reopens:t.filter(a=>a.kind===Bi)}}r(Ke,"ownerEvents");async function Ac(e,t){const{asked:n}=await fs(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(Ac,"unrecorded");async function fs(e){const t=await g(L(e),{topics:[],items:[]}),{answers:n,requests:a,reopens:i}=await Ke(e);return{owner:t,asked:(t.items||[]).map(o=>Pe(o,n,a,[],i))}}r(fs,"topicQuestionsAll");async function Ta(e,t){const n=await g(L(e),{topics:[],items:[]}),{answers:a,requests:i,reopens:o}=await Ke(e),s=(n.items||[]).filter(c=>c.topic===t).map(c=>Pe(c,a,i,[],o));return{owner:n,asked:s}}r(Ta,"topicQuestions");const ue=r((e,t)=>Ta(e,`item:${t}`),"cardQuestions"),An=r((e,t,n)=>qa(e,`item:${t}`,n),"resolveAnswered");async function qa(e,t,n){if(!e)return[];const{owner:a,asked:i}=await Ta(e,t),o=Ca(i);return o.length&&await Tt(e,a,o,n),await Wa(e,t),o.map(s=>`resolved: ${s}`)}r(qa,"resolveAnsweredOn");const Fa=r(async(e,t)=>!!e&&(await ue(e,t)).asked.some(n=>n.state==="open"),"hasOpenQuestion");function Ma(e,t,n){e.next=j(t,"--next"),n?e.next_on_questions=!0:delete e.next_on_questions}r(Ma,"setNext");async function Wa(e,t){const n=t.startsWith("item:")&&!t.startsWith("item:ask:")?t.slice(5):null;if(!n)return;let a;try{a=await v(e,n)}catch(c){if(c instanceof ie)return;throw c}const{source:i,wave:o,item:s}=a;s.next_on_questions&&((await ue(e,n)).asked.some(c=>c.state!=="resolved")||(delete s.next,delete s.next_on_questions,await y(i.file,o)))}r(Wa,"clearWaitedNext");async function ps(e,t){const{source:n,wave:a,item:i}=await v(e,t);i.next===void 0&&i.next_on_questions===void 0||(delete i.next,delete i.next_on_questions,await y(n.file,a))}r(ps,"clearNext");async function Ua(e,t,n,a){if(!e)return async()=>[];const{owner:i,asked:o}=await ue(e,t),s=o.filter(l=>l.state==="open").map(l=>l.id);s.length&&!a&&d(`${n}: ${t} has ${s.length===1?"an open question":"open questions"} ${s.join(", ")} with no answer yet; pass --force to close the card and resolve ${s.length===1?"it":"them"} too`);const c=o.filter(l=>l.state!=="resolved").map(l=>l.id);return async()=>(c.length&&await Tt(e,i,c,"Closed with the card"),c.map(l=>`resolved: ${l}`))}r(Ua,"closeQuestions");async function Dc(e,t,n,a,i){await v(e,t),j(n,"--text"),Y(a,o=>j(o,"--owner-step")),Y(i,o=>Ya(o)),await Ua(P(e),t,"land",!1)}r(Dc,"checkClose");async function Dn(e,t){const n=await g(L(e),{items:[]}),{answers:a,requests:i,reopens:o}=await Ke(e);return Ii((n.items||[]).map(s=>Pe(s,a,i,[],o)),t)}r(Dn,"unpickedPrototype");const ws=r(e=>e.length>120?`${e.slice(0,119)}\u2026`:e,"clip"),ms={ask:"text","ask-edited":"text","item-message":"text","owner-answer":"note"};async function Ha(e,t,n){const a=j(t,n).replace(/\s+/g," ");return(await K($.join(e,"data","events.jsonl"))).map(o=>String(o.data?.[ms[o.kind]]??"").replace(/\s+/g," ").toLowerCase()).some(o=>o.includes(a.toLowerCase()))||d(`${n} must quote the owner's words in an ask, card message or answer note on this board`),a}r(Ha,"ownerQuote");async function Rc(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 Ha(e,n,"--asked")}"`}r(Rc,"askedGate");async function lt(e,t,n="--from-ask"){return ge(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(lt,"checkAsk");function Rn(e,t,n=new Date().toISOString()){if(!vt[t])throw new Error(`no lane "${t}"`);const a=e.lane;return e.lane=t,e.lane_at=n,t==="done"?(e.closed_at=n,delete e.reopened_at):a==="done"&&(e.reopened_at=n),a}r(Rn,"placeIn");async function Nc(e,t,n,a){const{source:i,wave:o,item:s}=await v(e,t);if(s.lane===n)return null;const c=new Date().toISOString(),l=Rn(s,n,c);if(await y(i.file,o),l===void 0)return null;const u={ts:c,who:"claude",kind:Yn,text:`Moved to ${vt[n]}: ${j(a,"why")}`};return await V(U(e,t),u),u.text}r(Nc,"moveCard");const G=r(e=>e?.lane==="done"||!!e?.reopened_at,"closed"),Je=r(e=>!!e?.lane&&e.lane!=="todo","leftTodo"),Ec=r(async(e,t)=>G((await v(e,t)).item),"isFinished"),hs=["ready","grilling","prototyping"];function Nn(e){e==="staging"&&d("--state staging is set by the board, on an owner action; pass ready, grilling or prototyping"),e!==void 0&&!hs.includes(e)&&d("--state takes ready, grilling or prototyping")}r(Nn,"checkState");const gs="a card goes ready only from staging, which the board sets on the owner's Send, Implement or Reply";async function $s(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($s,"quickAsk");const ks=/^mark (?:it |this |the card )?ready\b/i;function bs(e,t,n){const a=[].concat(t).map(s=>un(s)||d(`--pr takes a PR number or owner/repo#<n>, not "${s}"`)),i=r((s,c)=>s.number===c.number&&(s.repo||n||null)===(c.repo||n||null),"same"),o=Ot(e);for(const s of a)o.some(c=>i(c,s))||o.push(s);delete e.pr,e.prs=o}r(bs,"addPRs");const H=r((e,t)=>String(t).includes("/")?String(t):`${e}/${t}`,"depKey");async function Ge(e,t,n){const a=(n.deps||[]).map(o=>H(t,o)),i=await Promise.all(a.map(o=>v(e,o).then(({item:s})=>s,()=>null)));return a.filter((o,s)=>i[s]&&!G(i[s]))}r(Ge,"unfinishedDeps");async function ys(e,t,n){const a=await Promise.all((await Ge(e,N(t).wave,n)).map(i=>X(e,i)));return n.blocked_until?[...a,n.blocked_until]:a}r(ys,"blockerNames");async function qt(e,t,n){n??=(await v(e,t)).item;const a=await Promise.all((await Ge(e,N(t).wave,n)).map(i=>X(e,i)));return[a.length&&`by ${a.join(", ")}`,n.blocked_until&&`until ${n.blocked_until}`].filter(Boolean).join(" and ")}r(qt,"blockWords");const xs=r((e,t)=>!Te(e)&&!e.pushed&&!e.merge_ready&&!G(e)&&(!!en(t)||t.some(n=>n?.started&&!n.done&&Xe(n))),"inProgress");async function vs(e,t){const n=[];for(const a of await T(e)){const i=await g(a.file,null);for(const o of Array.isArray(i?.items)?i.items:[])Je(o)||o.blocked_until||!(o.deps||[]).some(s=>H(a.slug,s)===t)||(await Ge(e,a.slug,o)).length||n.push(`${a.slug}/${o.id}`)}return n}r(vs,"unblockedBy");async function za(e,t,n,a){const i=new Set(a.match(/[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),o=new Set((n.deps||[]).map(s=>H(N(t).wave,s)));for(const s of await T(e)){const c=await g(s.file,null);for(const l of c?.items||[]){const u=`${s.slug}/${l.id}`;if(u===t||o.has(u)||G(l))continue;[l.id,St(c,l)].flatMap(p=>[p,`${s.slug}/${p}`]).some(p=>i.has(p))&&d(`--next names unfinished card ${u}; add it with --deps`)}}}r(za,"checkNextDeps");function _s(e,t){const n="a builder starts from it, so pass";t.size||d(`${e} has no size; ${n} --size small or large with --state ready`),t.plan||d(`${e} has no notes file; ${n} --notes-file <file.md> with --state ready`)}r(_s,"checkSizeAndNotes");async function Ss(e,t,n,a){_s(t,a);const i=await A.readFile(await Qe(n,a.plan),"utf8"),o=(await ue(e,t)).asked.filter(s=>s.answers.length&&!i.includes(s.id)&&!(s.prototype_gate&&!s.answers.at(-1).note)).map(s=>s.id);o.length&&d(`${t}'s notes leave out the answered question${o.length===1?"":"s"} ${o.join(", ")}; record each answer in ${a.plan}, naming its id, before it is ready`)}r(Ss,"checkNotesComplete");async function Qe(e,t,{missingOk:n=!1}={}){const a=r(s=>d(`a notes file is a .md file inside ${e}, named relative to it: "${t}" ${s}`),"bad");(typeof t!="string"||!t.trim())&&a("is empty"),($.isAbsolute(t)||t.split(/[\\/]/).includes(".."))&&a("leaves the folder"),$.extname(t).toLowerCase()!==".md"&&a("is not a .md file");const i=await A.realpath(e),o=await A.realpath($.resolve(e,t)).catch(()=>n?$.resolve(i,t):a("does not exist"));return o.startsWith(i+$.sep)||a("leaves the folder"),o}r(Qe,"planPath");async function Lc(e,t){const{source:n,wave:a,item:i}=await v(e,t);if(i.plan)return{wave:a,item:i,markdown:await A.readFile(await Qe(n.dir,i.plan),"utf8")};if(typeof i.notes=="string"&&i.notes.trim())return{wave:a,item:i,markdown:i.notes};d(`${t} has no notes`)}r(Lc,"readNotes");function Ka({install:e,installSha:t,installLabel:n,clearInstall:a}){if(e===void 0)return(t!==void 0||n!==void 0)&&d("--install-sha and --install-label go with --install <url>"),a?null:void 0;a&&d("pass --install or --clear-install, not both");let i;try{i=new URL(String(e).trim())}catch{i=null}(!i||!["https:","itms-services:"].includes(i.protocol))&&d(`--install takes an https:// or itms-services:// URL, not "${e}"`),i.protocol==="itms-services:"&&!(i.searchParams.get("action")==="download-manifest"&&/^https:\/\//.test(i.searchParams.get("url")||""))&&d("--install: an itms-services:// URL needs ?action=download-manifest&url=https://\u2026/manifest.plist");const o=t===void 0?void 0:/^[0-9a-f]{7,40}$/i.test(t)?t.toLowerCase():d(`--install-sha takes a commit sha of 7 to 40 hex digits, not "${t}"`);return{url:String(e).trim(),...n===void 0?{}:{label:j(n,"--install-label")},built_at:new Date().toISOString(),...o?{sha:o}:{}}}r(Ka,"installOf");async function Ic(e,{install:t,installSha:n}){const a=Ka({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(Ic,"setMainInstall");const Ja=r((e,t)=>e.length?`Merged PR${e.length>1?"s":""} ${qe(e,t)}.`:"Merged.","mergedLine");function js(e,t,n,a,i){return n?a??Ja(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??null)}r(js,"closingLine");function Os(e,t,n){const a=new Date().toISOString(),i=r((o,s)=>o.number===s.number&&(o.repo||n||null)===(s.repo||n||null),"same");e.prs=Ot(e).map(o=>{const s=t.find(c=>i(o,c));return s&&!o.merged_at?{...o,merged_at:s.at||a}:o}),delete e.pr}r(Os,"markMerged");async function Ga(e,t,{ticket:n,status:a,clearStatus:i,done:o,merged:s,start:c,handBack:l,next:u,clearNext:w,pr:p,pushed:f,mergeReady:m,ghSince:h,fromAsk:k,state:x,held:E,unheld:O,pickUpAt:B,notesFile:_,deps:S,blockedUntil:D,blockReason:F,parent:M,related:q,relate:I,unrelate:oe,install:pe,installSha:se,installLabel:$e,clearInstall:ke,title:Le,size:wt,model:ce,force:be,text:ye,who:mt,by:ht,command:Jt,ownerStep:W,ownerDone:Gt,check:ee,stopRun:vi},Fn){(a!==void 0||i)&&d(`--status${i?" and --clear-status are":" is"} gone: a card's lane is stored and moves itself. Close it with --done --text "<what was done, or why it closes without landing>"`),s&&(o=!0),q!==void 0&&(I!==void 0||oe!==void 0)&&d("pass --related, or --relate and --unrelate, not both"),(x!=="staging"||k===void 0)&&Nn(x),be&&!o&&d("--force goes with --done"),k!==void 0&&o&&d("--from-ask opens a card; close it in another call"),ye!==void 0&&j(ye,"--text"),W!==void 0&&j(W,"--owner-step");const Ve=ee===void 0||ee===""?ee:Ya(ee);ye!==void 0&&k===void 0&&!o&&d("--text goes with --from-ask or --done"),k!==void 0&&ye===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),u!==void 0&&w&&d("pass --next or --clear-next, not both"),typeof u=="string"&&!u.trim()&&([u,w]=[void 0,!0]);const Qt=Ps({held:E,unheld:O,pickUpAt:B}),gt=Ka({install:pe,installSha:se,installLabel:$e,clearInstall:ke});[o,c,l,u,w,p,f,m,h,k,x,Qt,_,S,D,F,M,q,I,oe,gt,Le,wt,ce,n,W,ee].every(R=>R===void 0||R===!1)&&d("nothing to set: pass --done, --next, --clear-next, --pr, --pushed, --gh-since, --from-ask, --state, --held, --unheld, --pick-up-at, --notes-file, --deps, --blocked-until, --block-reason, --parent, --related, --relate, --unrelate, --install, --clear-install, --title, --size, --model, --ticket or --check");const Q=k===void 0?void 0:await lt(e,k);Q&&await hn(e,Q);const Mn=Q!==void 0&&await $s(e,Q);x==="staging"&&!Mn&&Nn(x);const{source:we,wave:Ie,item:b}=await v(e,t);x==="ready"&&Mn&&!["staging","ready"].includes(b.state)&&d(`${t} came from a Quick build ask: file it --state staging, and the orchestrator judges it before it goes ready`),x==="ready"&&!["staging","ready"].includes(b.state)&&d(`${t} reads ${b.state||"no state"}, not staging; ${gs}`),Q&&b.from_ask&&b.from_ask!==Q&&d(`${t} came from ask "${b.from_ask}", so ask "${Q}" can't be its from-ask; to fold it in: item relate-ask ${Q} ${t} --summary "<line>"`),Q&&x===void 0&&!b.state&&d("--from-ask files a card, which reads New until it has a state: pass --state grilling or prototyping, or staging for a Quick build ask");const Wn=G(b);W!==void 0&&Wn&&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`),W!==void 0&&(b.owner_step=j(W,"--owner-step")),Gt&&delete b.owner_step,Ve!==void 0&&!b.owner_step&&d(`--check goes with an owner step, which ${t} doesn't have: pass --owner-step "<the step>" with it on builder done or land`),Ve&&(b.owner_check={check:Ve,since:b.owner_check?.check===Ve?b.owner_check.since:new Date().toISOString()}),(Ve===""||!b.owner_step)&&delete b.owner_check;const $t=await Promise.all((await z(we.dir,b.id)).map(R=>g(R,null))),_i=nn(b,$t);if(_!==void 0&&(await Qe(we.dir,_),b.plan=$.normalize(_)),Le!==void 0&&(b.title=j(Le,"--title")),Qa(b,{size:wt,model:ce}),S!==void 0){const R=await Xa(e,N(t).wave,S);R.length?b.deps=R:delete b.deps}const Un=D===""?b.blocked_until:void 0;if(D!==void 0&&(D===""?delete b.blocked_until:b.blocked_until=j(D,"--blocked-until")),F&&(!b.deps?.length&&!b.blocked_until&&d("--block-reason goes with a block: --deps or --blocked-until"),b.block_reason=j(F,"--block-reason")),(F===""||!b.deps?.length&&!b.blocked_until)&&delete b.block_reason,M!==void 0){const R=await ei(e,N(t).wave,t,M);R?b.parent=R:delete b.parent}const Hn=M!==void 0||q!==void 0||I!==void 0||oe!==void 0?await En(e,t):null;let Zt=q===void 0?null:await Ds(e,t,b,q);I!==void 0&&(b.related=[...new Set([...(b.related||[]).map(R=>H(N(t).wave,R)),...await Mt(e,t,I,"--relate")])]),oe!==void 0&&(Zt=await Rs(e,t,b,await Mt(e,t,oe,"--unrelate"))),Qt!==void 0&&Ft(b,Qt),c&&b.held&&d(b.pick_up_at?`${t} is held until ${b.pick_up_at}, when the board resumes it; start it early only with item set ${t} --unheld`:`${t} is held; start it only once the owner resumes it, with item set ${t} --unheld`);const Ye=p!==void 0||f!==void 0||m!==void 0;if((c||o||l||Ye)&&As(b),c){const R=await Ge(e,N(t).wave,b);R.length&&d(`${t} is blocked by ${R.join(", ")}; start it once ${R.length===1?"that is":"those are"} done`),b.blocked_until&&d(`${t} is blocked until ${b.blocked_until}; start it once that holds and the block is cleared with --blocked-until ""`)}const kt=c&&e?await Dn(e,t):null;kt&&d(`${t} has no take picked: the owner's answer to ${kt.id} picks none${kt.note?` ("${ws(kt.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 Si=o?await Ua(e,t,"--done",be):async()=>[],zn=o?js(e,t,s,ye,ne(e,Ie)):null;if(s&&Os(b,s,ne(e,Ie)),u!==void 0&&(Ma(b,u,await Fa(e,t)),await za(e,t,b,b.next)),w&&(delete b.next,delete b.next_on_questions),c&&u===void 0&&b.next_on_questions&&(delete b.next,delete b.next_on_questions),Ye&&!b.review_since&&(b.review_since=new Date().toISOString()),p!==void 0&&bs(b,p,ne(e,Ie)),f!==void 0){de().find(R=>he(R.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"),Te(b)&&d(`${t} has a PR; its PR says when it merges`);try{ao(f)}catch{d("--pushed takes the name of the branch you pushed")}b.pushed={branch:f,at:new Date().toISOString()},b.branch=f}if(m!==void 0&&(b.merge_ready={branch:m,at:new Date().toISOString()},b.branch=m),o&&delete b.merge_ready,h!==void 0&&((typeof h!="string"||!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(h)||Number.isNaN(Date.parse(h)))&&d("--gh-since takes an ISO-8601 UTC time such as 2026-10-23T11:00:00Z"),b.gh_since=h),Q&&(b.from_ask=Q),x==="ready"&&e){const R=(await ue(e,t)).asked.filter(Be=>Be.state==="open").map(Be=>Be.id);R.length&&d(`${t} has ${R.length===1?"an open question":"open questions"} ${R.join(", ")}; it is ready once the owner has answered ${R.length===1?"it":"them"}`),await Ss(e,t,we.dir,b)}x!==void 0&&(x!=="ready"&&["staging","ready"].includes(b.state)&&(b.round_at=new Date().toISOString()),b.state=x,x==="staging"?b.staged_at=new Date().toISOString():delete b.staged_at,delete b.unfiled),b.state==="ready"&&ks.test(b.next??"")&&d(`${t} would read ready with Next "${b.next}", which asks for the state it is already in; pass --next with what happens next, or --clear-next`),gt&&(b.install=gt),gt===null&&delete b.install,!_i&&!c&&nn(b,$t)&&d(`that leaves ${t} In Progress with nothing building and no Next; pass --next "<what happens next>"`);const bt=(S!==void 0||D)&&!o&&xs(b,$t)?await qt(e,t,b):"",ji=bt&&$t.some(R=>R?.started&&!R.done&&Xe(R))?await vi():[],Kn=ne(e,Ie);let Vt=null;const yt=r((R,Be)=>{b.lane!==R&&Rn(b,R)!==void 0&&Be&&(Vt=`Moved to ${vt[R]}: ${Be}`)},"moveTo");o?yt("done",null):bt?yt("todo",null):Ye&&b.lane!=="done"?yt("review",p!==void 0?`PR ${qe([].concat(p).map(un),Kn)} opened`:f!==void 0?`${f} pushed for its merge`:"verified, it waits for your Merge"):c&&!G(b)&&yt("progress","a build started"),(n!==void 0||Q&&!b.ticket&&!b.linear?.length)&&await Ln(e,t,Ie,b,n,Fn,we.dir),await y(we.file,Ie),Vt&&await V(U(e,t),{ts:b.lane_at,who:"claude",kind:Yn,text:Vt}),bt&&(await qn(we.dir,b.id,!1,{endHandBack:!0}),await V(U(e,t),{ts:new Date().toISOString(),who:"claude",text:`Blocked ${bt}: back to Todo. It starts again once nothing blocks it.`})),Zt&&await Zt(),Hn&&await ni(e,t,Hn,await En(e,t));const xe=[...ji,...await Si()];(_!==void 0||x==="ready"&&e)&&xe.push(...await An(e,t,"Recorded in the card's notes")),c&&xe.push(...await An(e,t,"Acted on: build started")),Q&&xe.push(...(await J(e,t,{text:ye})).lines),zn&&e&&xe.push(...(await J(e,t,{text:zn,who:mt,by:ht,command:Jt})).lines);const Jn=b.owner_step&&o?null:Zi({start:c,done:o,handover:Ye}),Gn=Jn&&await Vi(await ai(e),b,Jn,{...Fn,repo:Kn});Gn&&xe.push(Gn),(Ye||o||l)&&(await qn(we.dir,b.id,o,l?{handedBack:new Date().toISOString()}:{endHandBack:!0}),await Bt(e,t));const Yt=Q?await Re(e):{};if(Z(Yt,Q)!==void 0&&(delete Yt[Q],await y(ve(e),{links:Yt})),Un&&!G(b)&&!(await Ge(e,N(t).wave,b)).length&&(await V(U(e,t),{ts:new Date().toISOString(),who:"claude",text:`Unblocked: ${Un}.`}),await re({dir:e},"card-unblocked",{key:t}),xe.unshift(`unblocked: ${t}`)),!o||Wn)return{file:we.file,lines:xe};const Qn=await vs(e,t),Oi=new Date().toISOString();for(const R of Qn)await V(U(e,R),{ts:Oi,who:"claude",text:`Unblocked: ${await X(e,t)} is done.`}),await re({dir:e},"card-unblocked",{key:R});return{file:we.file,lines:[...Qn.map(R=>`unblocked: ${R}`),...xe]}}r(Ga,"changeItem");function Qa(e,{size:t,model:n}){for(const[a,i,o]of[["size",t,["small","large"]],["model",n,["opus","sonnet"]]])i!==void 0&&(i===""?delete e[a]:e[a]=o.includes(i)?i:d(`--${a} takes ${o.join(" or ")}, or "" to clear it`))}r(Qa,"setBuildChoice");function Ps({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:Za(n)}:e?{until:null}:t?null:void 0}r(Ps,"holdOf");function Za(e){const t=typeof e=="string"&&Gi.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 Mac\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(Za,"pickUpTime");function As(e){delete e.held,delete e.pick_up_at}r(As,"clearHold");function Ft(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(Ft,"setHold");async function Bc(e,t,n,{announce:a=!0}={}){const{source:i,wave:o,item:s}=await v(e,t);if(a){const c=new Date(n).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});await V(U(e,t),{ts:new Date().toISOString(),who:"claude",text:`Resumed: its hold until ${c} ended.`}),!s.blocked_until&&!(await Ge(e,N(t).wave,s)).length&&await re({dir:e},"card-unblocked",{key:t,until:n})}Ft(s,null),await y(i.file,o)}r(Bc,"pickUp");async function Va(e,t,n){const a=String(n?.key||""),{wave:i,id:o}=N(a),{source:s,wave:c}=await Me(e,i),l=n.shared_by?n.shared_by.name||n.shared_by.login:void 0,u={text:Oe.done,who:"love",by:l,command:"done"},w=c.items.find(h=>h.id===o)||d(`wave "${i}" has no item "${o}"`);if(!(t==="card-done"&&!!w.owner_step)&&w.lane==="done")return null;if(t==="card-done")return fe(e,a,{done:!0,force:!0,ownerDone:!0,...u});const f=n.hold?typeof n.until=="string"?new Date(n.until).toISOString():null:void 0;Ft(w,n.hold?{until:f}:null),await y(s.file,c),n.hold&&(await qn(s.dir,o,!1,{held:!0}),await Bt(e,a));const m=f&&new Date(f).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});return J(e,a,n.hold?{text:m?`${Oe.hold} until ${m}`:Oe.hold,who:"love",by:l,command:"hold"}:{text:Oe.resume,who:"love",by:l,command:"resume"})}r(Va,"recordOwnerTap");function Ya(e){try{return ko($o(e))}catch(t){return d(t.message)}}r(Ya,"checkSpec");async function Cc(e,t,n,a){const{item:i}=await v(e,t);return!i.owner_step||i.owner_check?.check!==n?null:fe(e,t,{done:!0,force:!0,ownerDone:!0,text:`Check passed: ${a}. Closed.`})}r(Cc,"passOwnerCheck");async function Tc(e,t,n,a){const{source:i,wave:o,item:s}=await v(e,t);return!s.owner_step||s.owner_check?.check!==n?!1:(s.owner_check.broken=a,await y(i.file,o),await J(e,t,{text:`The check for "${s.owner_step}" can't run (${a}), so it stopped; Claude is asked to fix it. Done still closes the card.`}),await re({dir:e},"owner-check-broken",{key:t,check:n,error:a}),!0)}r(Tc,"breakOwnerCheck");async function Xa(e,t,n){const a=[...new Set(String(n).split(",").map(i=>i.trim()).filter(Boolean))];for(const i of a)await v(e,H(t,i));return a}r(Xa,"depList");async function ei(e,t,n,a){if(!String(a).trim())return null;const i=H(t,String(a).trim());i===n&&d("--parent: a card can't be its own Parent");for(let o=i;o;){const s=o===i?await v(e,o):await v(e,o).catch(l=>{if(l instanceof ie)return null;throw l});if(!s)break;const{item:c}=s;c.parent&&H(N(o).wave,c.parent)===n&&d(`--parent: ${i} is a child of ${n}, so it can't be its Parent`),o=c.parent?H(N(o).wave,c.parent):null}return i}r(ei,"parentKey");async function Mt(e,t,n,a){const i=t.split("/")[0],o=[...new Set(String(n).split(",").map(s=>s.trim()).filter(Boolean).map(s=>H(i,s)))];o.includes(t)&&d(`${a}: a card can't be related to itself`);for(const s of o)await v(e,s);return o}r(Mt,"relatedKeys");const ti=r((e,t,n)=>async()=>{for(const a of await T(e)){const i=await g(a.file,null);let o=!1;for(const s of Array.isArray(i?.items)?i.items:[]){const c=`${a.slug}/${s.id}`;c===t||!n(c)||!(s.related||[]).some(l=>H(a.slug,l)===t)||(s.related=s.related.filter(l=>H(a.slug,l)!==t),s.related.length||delete s.related,o=!0)}o&&await y(a.file,i)}},"dropLinksTo");async function En(e,t){const n=new Set;let a=null;for(const i of await T(e))for(const o of(await g(i.file,null))?.items||[]){const s=`${i.slug}/${o.id}`,c=(o.related||[]).map(l=>H(i.slug,l));s===t?(c.forEach(l=>n.add(l)),o.parent&&(a=H(i.slug,o.parent))):c.includes(t)&&n.add(s)}return{related:n,parent:a}}r(En,"linksOf");async function ni(e,t,n,a){const i=r(async(o,s,c=o,l=o)=>{await J(e,t,{text:Oe[c],command:c,card:s}),await J(e,s,{text:Oe[l],command:l,card:t})},"both");for(const o of a.related)n.related.has(o)||await i("related",o);for(const o of n.related)a.related.has(o)||await i("unrelated",o);a.parent&&a.parent!==n.parent&&await i("followup-of",a.parent,"followup-of","followup")}r(ni,"postLinkLines");async function Ds(e,t,n,a){const i=await Mt(e,t,a,"--related");return i.length?n.related=i:delete n.related,ti(e,t,o=>!i.includes(o))}r(Ds,"setRelated");function Rs(e,t,n,a){const i=(n.related||[]).filter(o=>!a.includes(H(N(t).wave,o)));return i.length?n.related=i:delete n.related,ti(e,t,o=>a.includes(o))}r(Rs,"unrelateCards");const ne=r((e,t)=>e&&de().find(n=>he(n.dir,e))?.github||t.repo,"boardRepoOf");async function qc(e,t){const{wave:n,item:a}=await v(e,t);Te(a)||d(`${t} has no PR`);const i=Ae(a,ne(e,n));i.length||d(`${t} has no repo`);const o=a.gh_since||"1970-01-01T00:00:00Z";try{const s=_e(await te(e)).github_login||await co();return await Promise.all(i.map(async({repo:c,number:l})=>({repo:c,...na(await ta({repo:c,pr:l,since:o}),{pr:l,since:o,me:s})})))}catch(s){d(s.message)}}r(qc,"prReport");async function Fc(e,t,{replyTo:n,body:a}){const i=n===void 0?"pr comment":"pr reply",o=un(t)||d(`${i} takes a PR number or owner/repo#<n>`);n!==void 0&&!/^[1-9]\d*$/.test(n)&&d("pr reply takes the id of the review comment it answers");const s=j(a,"--body").trim();(s.startsWith(tt.trim())||s.includes(dn))&&d(`${i} adds the ${tt.trim()} prefix and the claude-bridge marker itself; pass only your words`);const c=o.repo||ne(P(e),{})||d(`${i} needs a board on GitHub; this board has no repo there`);try{const l=await lo({repo:c,pr:o.number,replyTo:n,words:s,login:_e(await te(e)).github_login}),u=await xa(e,nt({repo:c,number:o.number}),"comment",`Comment on #${o.number}: ${s}`);return{lines:[`posted on PR ${o.repo?t:`#${t}`}: ${l}`,...u.length?[`queued for ${u.join(", ")}'s Slack thread`]:[]]}}catch(l){d(l.message)}}r(Fc,"postPR");const ai=r(async e=>Pa((await te(e)).tickets),"boardSource");async function Mc(e,t,{afterApproval:n,confirmed:a,tapped:i}={}){P(e);const{wave:o,item:s}=await v(e,t);Te(s)||d(`${t} has no PR`),G(s)&&d(`${t} is done`);const c=await qt(e,t,s);c&&(i&&await J(e,t,{text:`Not merged: blocked ${c}.`}),d(`Not merged: blocked ${c}.`));const l=ne(e,o),u=Ae(s,l);u.length||d(`${t} has no repo`);const w=new Date().toISOString();let p;try{p=await Promise.all(u.map(S=>ta({repo:S.repo,pr:S.number,since:w})))}catch(S){d(S.message)}const f=p.map(({view:S})=>S);if(po(f)){const S=u.map((D,F)=>({...D,at:f[F].mergedAt})).filter((D,F)=>f[F].state==="MERGED");return fe(e,t,{merged:S,text:`PR${S.length>1?"s":""} ${qe(S,l)} ${S.length>1?"were":"was"} already merged.`})}const m=u.map((S,D)=>({...S,...p[D]})).filter(({view:S})=>S.state==="OPEN"),h=pn(o,(await te(e)).reviewers||[]).length>0,k=[];let x=!1,E=!1;for(const S of m){const{view:D,reviews:F}=S,{failing:M,pending:q}=na(S,{pr:S.number,since:w,me:null}),I=Li({...D,checks:{state:M.length?"fail":q?"pending":"pass",failing:M,pending:q}},{needsApproval:h}),oe=F.filter(se=>se.state==="APPROVED").sort((se,$e)=>String(se.submitted_at).localeCompare(String($e.submitted_at))).at(-1)?.commit_id;let pe=!!(oe&&D.headRefOid&&oe!==D.headRefOid);pe&&n===void 0&&Ms(s,oe,D.headRefOid)&&(pe=!1,E=!0),x||=pe,pe&&n===void 0&&I.push('commits after the approval: pass --after-approval "<why they are trivial>" when they are, else ask the approver to look again'),k.push(...u.length>1?I.map(se=>`${qe([S],l)} ${se}`):I)}m.length||k.push(u.length>1?"every PR is closed":"PR is closed"),n!==void 0&&!x&&d("--after-approval: no commit came after the approval; drop it");const O=s.manual_steps?.length?a===void 0?null:await Ha(e,a,"--confirmed"):void 0;if(a!==void 0&&O===void 0&&d(`--confirmed: ${t} has no manual steps; drop it`),O===null&&k.push(`manual steps: merge only on the owner's word, with --confirmed "<their words>"`),k.length){const S=`Not merged: ${k.join("; ")}.`;i&&await J(e,t,{text:S}),d(S)}const B=[];for(const S of m){const D=await new Promise(F=>{Zn("gh",["pr","merge",String(S.number),"--repo",S.repo,"--squash"],{encoding:"utf8",timeout:12e4},(M,q,I)=>F(M?String(I||M.message).trim().split(`
|
|
26
|
-
`)[0]:null))});D&&d(`gh pr merge ${qe([S],l)} failed: ${D}${B.length?` (merged ${qe(B,l)} before it)`:""}`),B.push(S)}const _=[Ja(B,l),n!==void 0&&`After the approval: ${j(n,"--after-approval")}`,E&&`After the approval: only the board's rebase onto ${s.rebased.at(-1).base}.`,O&&`The owner's word on the manual steps: "${O}"`];return fe(e,t,{merged:B,text:_.filter(Boolean).join(" ")})}r(Mc,"mergeCard");async function Wc(e,t){return(await v(e,t)).item.merge_ready||null}r(Wc,"heldMerge");async function Uc(e,t){const{source:n,wave:a,item:i}=await v(e,t);(!i.merge_ready||G(i))&&d(`${t} isn't waiting for its Merge`);const o=await qt(e,t,i);return o&&d(`${t} is blocked ${o}, so its Merge waits until nothing blocks it`),i.merge_ready.tapped=new Date().toISOString(),await y(n.file,a),i.merge_ready}r(Uc,"tapMerge");async function Hc(e,t){const{source:n,item:a}=await v(e,t);return(await Promise.all((await z(n.dir,a.id)).map(o=>g(o,null)))).filter(o=>o?.worktree?.path).sort((o,s)=>String(s.started).localeCompare(String(o.started)))[0]?.model}r(Hc,"builderModel");async function Ln(e,t,n,a,i,o,s){try{const c=await te(e),l=cn(c.tickets||{source:"internal"}),u=r((m,h)=>m.internal===!0?String(h.id).replace(/^S(\d+)$/,`${l.prefix}-$1`):null,"ownId");i===void 0&&l.source==="internal"&&ln(l,u(n,a))&&(i=u(n,a));let w=0;if(l.source==="internal")for(const m of await T(e)){const h=await g(m.file,{});for(const k of h.items||[]){const x=[k.ticket,u(h,k)];if(w=Math.max(w,...x.map(E=>ln(l,E)||0)),i!==void 0&&`${m.slug}/${k.id}`!==t&&x.includes(i))throw Object.assign(new ie(`${i} is already the ticket of ${m.slug}/${k.id}`),{held:!0})}}const p=i===void 0&&l.source!=="internal"&&a.plan?await A.readFile(await Qe(s,a.plan),"utf8"):a.notes||"",f=await Qi(l,{ticket:i,title:a.title,body:p,repo:ne(e,n),next:w+1},o);a.ticket=f.ticket,f.created&&l.source!=="internal"?a.ticket_created=!0:delete a.ticket_created}catch(c){if(c instanceof ie)throw c;d(c.message)}}r(Ln,"assignTicket");const Ze=r((e,t)=>jt($.join(e||At(),"tickets"),t,{heartbeat:!0}),"ticketWrite"),fe=r((e,t,n,a)=>n.ticket!==void 0||n.fromAsk!==void 0?Ze(e,()=>Ga(e,t,n,a)):Ga(e,t,n,a),"setItem"),zc=r((e,t,n,a)=>Ze(e,()=>Ns(e,t,n,a)),"addItem");async function Ns(e,t,{title:n,notes:a,deps:i,notesFile:o,ticket:s,pickUpAt:c,parent:l,size:u,model:w,state:p,relate:f,intake:m=!1},h){const{wave:k,id:x}=t.includes("/")?N(t):{wave:t};x!==void 0&&ge(x,"the item id"),Nn(p),p==="ready"&&d("item add files a card grilling or prototyping; it reaches ready through staging, on the owner's Send, Implement or Reply");const{source:E,wave:O}=await Me(e,k);x!==void 0&&O.items.some(D=>D.id===x)&&d(`wave "${k}" already has an item "${x}"`);const B=new Date().toISOString(),_={id:x,title:j(n,"--title"),created:B,lane:"todo",lane_at:B};l!==void 0&&(_.parent=await ei(e,k,null,l)||d("--parent takes a card, such as <wave>/<id>")),a!==void 0&&(_.notes=j(a,"--notes")),Qa(_,{size:u,model:w}),i!==void 0&&(_.deps=await Xa(e,k,i)),o!==void 0&&(await Qe(E.dir,o),_.plan=$.normalize(o)),c!==void 0&&Ft(_,{until:Za(c)}),f!==void 0&&(_.related=await Mt(e,t,f,"--relate")),p!==void 0?_.state=p:m&&e&&(_.unfiled=!0),await Ln(e,t,O,_,s,h,E.dir),x===void 0&&(_.id=Xi(_.ticket),O.items.some(D=>D.id===_.id)&&d(`wave "${k}" already has an item "${_.id}"; link ${_.ticket} with item add ${k}/<id> --ticket ${_.ticket}`));const S=`${k}/${_.id}`;return O.items.push(_),await y(E.file,O),_.unfiled&&await re(de().find(D=>he(D.dir,e))||{dir:e},"card-added",{key:S,title:_.title}),e&&await ni(e,S,{related:new Set,parent:null},await En(e,S)),{file:E.file,key:S,lines:[]}}r(Ns,"createItem");const Kc=r((e,{dryRun:t=!1}={},n)=>Ze(P(e),()=>Es(e,t,n)),"backfillTickets");async function Es(e,t,n){const a=await ai(e)||d("this board has no ticket source yet; pick one in Board settings first"),i=new Set,o=[];for(const w of await T(e)){const p=await g(w.file,null);if(Array.isArray(p?.items)){o.push({entry:w,wave:p});for(const f of p.items)f.ticket&&i.add(f.ticket),p.internal===!0&&a.source==="internal"&&i.add(String(f.id).replace(/^S(\d+)$/,`${a.prefix}-$1`))}}const s=[],c={linked:0,created:0,failed:0},l=r((w,p)=>p.ticket&&!(a.source!=="internal"&&w.internal===!0&&p.ticket===p.id),"ticketed");for(const{entry:w,wave:p}of o)for(const f of p.items){if(l(p,f)||G(f))continue;const m=`${w.slug}/${f.id}`,h=[...Yi(a,f.title),...(f.linear||[]).filter(k=>ln(a,k))].find(k=>a.source!=="internal"||!i.has(k));if(t){c[h?"linked":"created"]++,s.push(`${m} would ${h?`link ${h}`:"get a new ticket"}`);continue}try{await Ln(e,m,p,f,h,n,w.dir),await y(w.file,p),i.add(f.ticket),c[h?"linked":"created"]++,s.push(`${m} ${h?"linked":"created"} ${f.ticket}`)}catch(k){if(!(k instanceof ie))throw k;c.failed++,s.push(`${m} failed: ${k.message}`)}}const u=t?`would link ${c.linked}, create ${c.created}`:`linked ${c.linked}, created ${c.created}, failed ${c.failed}`;return{lines:[...s,`backfill${t?" (dry run)":""}: ${u}`]}}r(Es,"backfill");const Jc=r(e=>Ze(e,async()=>{const t=[];for(const n of await T(e)){const a=await g(n.file,null);if(!Array.isArray(a?.items)||!Array.isArray(a.followups))continue;const i=a.followups.find(l=>a.items.some(u=>u.id===l.id));i&&d(`${n.slug}: follow-up ${i.id} has the id of a card; rename one, then restart the board`);const o=[];for(const{id:l,title:u,from:w,filed:p,done:f,...m}of a.followups){const h=f?e&&(await K(U(e,`${n.slug}/${l}`))).map(k=>k.ts).filter(Boolean).sort().pop()||p:null;o.push({id:l,title:u,...p?{created:p}:{},...w?{parent:H(n.slug,w)}:{},...f?{lane:"done",...h?{lane_at:h,closed_at:h}:{}}:{},...m})}const{followups:s,...c}=a;await y(n.file,{...c,items:[...a.items,...o]}),t.push(`${n.slug}: ${o.length} follow-ups moved to cards (${o.filter(l=>l.lane==="done").length} done)`)}return t}),"migrateFollowups");function ii(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=Ot(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&&Xe(i)).sort((i,o)=>String(i.started).localeCompare(String(o.started))).at(-1);return e.status==="checking"&&a&&(a.handed_back=n),delete e.status,delete e.status_at,delete e.merged_at,!0}r(ii,"statusToFacts");const Gc=r(e=>Ze(e,async()=>{const t=[];for(const n of await T(e)){const a=await g(n.file,null);let i=0;for(const o of Array.isArray(a?.items)?a.items:[]){const s=o.status==="checking"?await z(n.dir,o.id):[],c=await Promise.all(s.map(l=>g(l,null)));if(ii(o,c.filter(Boolean))){i++;for(const[l,u]of c.entries())u?.handed_back&&await y(s[l],u)}}i&&(await y(n.file,a),t.push(`${n.slug}: ${i} cards' statuses became lanes and facts`))}return await Ut(e,async()=>{const n=await dt(e),a=n.filter(i=>i.item&&ii(i.item)).length;a&&(await zt(e,n),t.push(`archive: ${a} cards' statuses became lanes and facts`))}),t}),"migrateStatuses"),Qc=r(e=>Ze(e,async()=>{const t=[];for(const n of await T(e)){const a=await g(n.file,null),i=(Array.isArray(a?.items)?a.items:[]).filter(o=>o.implement_at);if(i.length){for(const o of i)o.state!=="ready"&&Object.assign(o,{state:"staging",staged_at:o.implement_at}),delete o.implement_at;await y(n.file,a),t.push(`${n.slug}: ${i.length} Implement asks moved to staging or ready`)}}return t}),"stageImplementAsks"),Wt=r(e=>$.join(e,"data","done-archive.json"),"archiveFile"),Ut=r((e,t)=>jt($.join(e,"done-archive"),t,{heartbeat:!0}),"archiveWrite"),dt=r(async e=>(await g(Wt(e),{cards:[]})).cards||[],"readArchive"),Ht=r(e=>`${e.closed_at||""}|${e.key}`,"archiveRank"),Ls=r((e,t)=>Ht(e)<Ht(t)?1:Ht(e)>Ht(t)?-1:0,"byClosed"),In=r(e=>$.join(e,"data","done-archive-count.json"),"archiveCountFile");async function Zc(e){const t=(await g(In(e),null))?.count;if(Number.isInteger(t))return t;const n=(await dt(e)).length;return await y(In(e),{count:n}),n}r(Zc,"archiveCount");async function zt(e,t){t.length?await y(Wt(e),{cards:t.sort(Ls)}):await A.rm(Wt(e),{force:!0}),await y(In(e),{count:t.length})}r(zt,"writeArchive");const Vc=r((e,t)=>Ut(e,async()=>{const n=new Map(t.map(p=>[p.key,p])),a=new Date().toISOString(),i=[],o=[];for(const p of await T(e)){const f=await g(p.file,null);if(!Array.isArray(f?.items))continue;const m=f.items.filter(h=>n.has(`${p.slug}/${h.id}`)&&h.lane==="done");if(m.length){for(const h of m)i.push({...n.get(`${p.slug}/${h.id}`),wave:p.slug,archived_at:a,item:h});o.push([p.file,{...f,items:f.items.filter(h=>!m.includes(h))}])}}if(!i.length)return[];const s=await g(L(e),{items:[]}),c=new Map(i.map(p=>[`item:${p.key}`,p]));for(const p of i)p.questions=(s.items||[]).filter(f=>f.topic===`item:${p.key}`&&f.resolved);const l=await Re(e);for(const p of i)p.asks=p.item.from_ask&&Z(l,p.item.from_ask)===void 0?[p.item.from_ask]:[];const u=new Set(i.map(p=>p.key));await zt(e,[...(await dt(e)).filter(p=>!u.has(p.key)),...i.map(({key:p,wave:f,archived_at:m,closed_at:h,item:k,questions:x,asks:E})=>({key:p,wave:f,archived_at:m,closed_at:h,item:k,questions:x,asks:E}))]),i.some(p=>p.asks.length)&&await y(ve(e),{links:{...l,...Object.fromEntries(i.flatMap(p=>p.asks.map(f=>[f,p.key])))}});for(const[p,f]of o)await y(p,f);const w=(s.items||[]).filter(p=>!(c.has(p.topic)&&p.resolved));return w.length!==(s.items||[]).length&&await y(L(e),{...s,items:w}),[...u]}),"archiveCards");async function Is(e,t){return!e||!me(Wt(e))?!1:Ut(e,async()=>{const n=await dt(e),a=n.find(p=>p.key===t);if(!a)return!1;const i=(await T(e)).find(p=>p.slug===a.wave),o=i&&await g(i.file,null);if(!Array.isArray(o?.items))return!1;o.items.some(p=>p.id===a.item.id)||await y(i.file,{...o,items:[...o.items,a.item]});const s=await g(L(e),{items:[]}),c=new Set((s.items||[]).map(p=>p.id)),l=(a.questions||[]).filter(p=>!c.has(p.id));l.length&&await y(L(e),{...s,items:[...s.items||[],...l]});const u=await Re(e),w=(a.asks||[]).filter(p=>Z(u,p)===t);return w.length&&await y(ve(e),{links:Object.fromEntries(Object.entries(u).filter(([p])=>!w.includes(p)))}),await zt(e,n.filter(p=>p!==a)),!0})}r(Is,"restoreArchived");const oi=["push_tag","no_change"],si=r(e=>Object.fromEntries(Object.entries(e).filter(([t])=>!oi.includes(t))),"stripQuestion"),ri=r(({summary:e,...t})=>t,"stripItem"),ci=r(e=>oi.some(t=>t in e),"holdsDead"),Yc=r(e=>Ut(e,async()=>{for(const a of await T(e)){const i=await g(a.file,null);Array.isArray(i?.items)&&i.items.some(o=>"summary"in o)&&await y(a.file,{...i,items:i.items.map(ri)})}const t=await g(L(e),null);(t?.items||[]).some(ci)&&await y(L(e),{...t,items:t.items.map(si)});const n=await dt(e);n.some(a=>"card"in a||"summary"in a.item||(a.questions||[]).some(ci))&&await zt(e,n.map(({card:a,...i})=>({...i,...a?{closed_at:i.closed_at||a.sortAt}:{},item:ri(i.item),questions:(i.questions||[]).map(si)})))}),"stripDeadFields"),li=r(e=>`lineup-${e}.md`,"lineupNoteFile"),Bs=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}"`),"lineupSlug");async function Kt(e,t){for(const n of await T(e)){const a=await g(n.file,null),i=(a?.lineups||[]).find(o=>o.slug===t);if(i)return{source:n,wave:a,lineup:i}}return d(`this board has no wave "${t}"; lineup create makes one`)}r(Kt,"findLineup");async function Cs(e,t){(await te(e)).items!=="waves"&&d(`topic wave:${t} is for a board over ~/.claude/waves; a lineup's triage uses lineup:<slug>`),await Me(e,t)}r(Cs,"triagedWave");function ut(e,t,n,a){return n.map(i=>{const{wave:o,id:s}=N(i);o!==e.slug&&d(`${i} is not in wave file "${e.slug}"; a wave's cards share one file`),t.items.some(l=>l.id===s)||d(`wave "${o}" has no item "${s}"`);const c=(t.lineups||[]).find(l=>l.slug!==a&&!l.finished_at&&tn(l).flat().includes(s));return c&&d(`${i} is already in the wave "${c.slug}"; lineup remove ${c.slug} ${i} first`),s})}r(ut,"lineupCards");function di(e){const t=[e??[]].flat().map(n=>String(n).split(",").map(a=>a.trim()).filter(Boolean));return(!t.length||t.some(n=>!n.length))&&d("--phase takes one phase's cards, as <wave>/<id>,<wave>/<id>, once per phase in build order"),t}r(di,"phaseKeys");function ui(e,t,n,a){const i=new Set(n.map(o=>`${e.slug}/${o}`));for(const o of n){const s=t.items.find(u=>u.id===o),c=(s.deps||[]).filter(u=>!i.has(H(e.slug,u))),l=a.findIndex(u=>u.includes(o));l>0&&c.push(...a[l-1]),c.length?s.deps=c:delete s.deps}}r(ui,"chain");async function fi(e,t,n,a,i){i!==void 0&&await at($.join(t.dir,li(a.slug)),`${j(i,"the note").replace(/\n*$/,`
|
|
27
|
-
`)}`
|
|
20
|
+
## ${t}
|
|
21
|
+
${n.replace(/^\n+|\s+$/g,"")}
|
|
22
|
+
`,"withSection");async function at(e,t,n,a){const{source:i,item:{id:o}}=await x(e,t);return Ls(i.dir,o,async()=>{const{source:s,wave:c,item:d}=await x(e,t);let u;d.plan?(u=await Yt(s.dir,d.plan,{missingOk:!0}),fe(u)||await U(e,t,{text:`The card's notes file ${d.plan} was missing, so the board started a new one.`})):(d.plan=qn(s.dir,d.id),u=m.join(s.dir,d.plan),await y(s.file,c));const p=fe(u)?await P.readFile(u,"utf8"):`# ${d.title}
|
|
23
|
+
`;return await Fn(u,Ts(p,Cs(n),a)),d.plan})}r(at,"appendNotes");async function Va(e){const{tickets:t}=be(await ie(e)),n=[],a=new Map;for(const i of await F(e)){const o=await $(i.file,null);a.set(i.slug,{source:i,wave:o||{}});for(const s of o?.items||[])s.plan&&n.push({key:`${i.slug}/${s.id}`,ref:Qe(o,s,t),title:s.title,created:s.created||"",archived:!1,dir:i.dir,plan:s.plan})}for(const i of e?await Me(e):[]){if(!i.item?.plan)continue;const o=a.get(i.wave);n.push({key:i.key,ref:Qe(o?.wave||{},i.item,t),title:i.item.title,created:i.item.created||"",archived:!0,dir:o?.source.dir||m.join(e,"data"),plan:i.item.plan})}return n}r(Va,"cardsWithNotes");const Xa=r(e=>P.realpath(m.resolve(e.dir,e.plan)).catch(()=>null),"realNotes");async function ei(e,t,n,a){if(t==="-")return to(0,"utf8");const i=await P.realpath(t).catch(()=>l(`${n} ${t}: no such file`));for(const o of await Va(e))await Xa(o)===i&&l(`${n} ${t} is the notes file of ${o.ref} "${o.title}" (${o.key}), and a card's notes are its own; ${a(o)}`);return P.readFile(i,"utf8")}r(ei,"refuseCardNotes");const Mn="write a draft of your own outside the board's data folder";async function nd(e,t,{heading:n,file:a,from:i}){let o;if(i!==void 0){i===t&&l(`${t} can't fold its own notes into itself; pass --from <the other card>`);const[d,u]=await Promise.all([x(e,t),x(e,i)]),[p,w]=await Promise.all([d,u].map(({source:f,item:h})=>h.plan?P.realpath(m.resolve(f.dir,h.plan)).catch(()=>null):null));p&&p===w&&l(`${i} and ${t} name one notes file, ${d.item.plan}, from before the board named each card's own; the board splits it as it starts`),o=(await tr(e,i)).markdown,n??=`Folded from ${await K(e,i)} "${u.item.title}"`}else o=await ei(e,a,"--file",d=>d.key===t?`${Mn}, holding only the new section`:`${Mn}, or fold that card's notes in with item notes ${t} --from ${d.key}`);o.trim()||l(`${i??a} is empty`);const s=await at(e,t,n,o),c=await Jt(e,t,"Recorded in the card's notes");return{file:s,heading:n,lines:c}}r(nd,"addNotes");const ad=r(e=>Ee(e,async()=>{const t=await Va(e),n=new Map;for(const i of t){const o=await Xa(i);o&&n.set(o,[...n.get(o)||[],i])}const a=[];for(const[i,o]of n){const[s,...c]=o.sort((d,u)=>d.created<u.created?-1:d.created>u.created?1:d.key<u.key?-1:1);for(const d of c.filter(u=>!u.archived)){const{source:u,wave:p,item:w}=await Te(e,I(d.key).wave).then(h=>({...h,item:h.wave.items.find(g=>g.id===I(d.key).id)})),f=w.plan;w.plan=qn(u.dir,w.id),await Fn(m.join(u.dir,w.plan),await P.readFile(i,"utf8")),await y(u.file,p),await U(e,d.key,{text:`This card shared its notes file ${m.normalize(f)} with ${s.ref}, so the board gave it its own copy, ${w.plan}.`}),a.push(`${d.key}: its own notes, ${w.plan}, copied from ${m.normalize(f)}, which ${s.key} keeps`)}await P.chmod(i,Za)}return a}),"splitSharedNotes");async function qs(e,t){const{source:n,item:a}=await x(e,t);return Ne(a)||Ce(a)||a.pushed||(await Promise.all((await H(n.dir,a.id)).map(o=>$(o,null)))).some(o=>o?.started&&!o.done&&o.kind!==we)||(await de(e,t)).asked.some(o=>o.state==="open")||(await $($e(e),{}))[t]?!1:!await Hn(e,t)}r(qs,"stageable");async function Wn(e,t){const{source:n,wave: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 z(M(e,t),{ts:new Date().toISOString(),who:"claude",command:"staged",text:he.staged}))}r(Wn,"stage");const ti=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 Fs(e,t,n){const{item:a}=await x(e,t);a.held&&await xi(e,"card-hold",{key:t,hold:!1});const{owner:i,asked:o}=await de(e,t),s=o.filter(d=>d.state==="open").map(d=>d.id);s.length&&await Kt(e,i,s,"Skipped: you tapped Implement");const c=ti(n);c&&await at(e,t,`Implement (owner, ${yt()}), verbatim`,c),await Wn(e,t)}r(Fs,"stageImplement");async function id(e,t,n){const a=ti(n);a&&await at(e,t,`Reply (owner, ${yt()}), verbatim`,a),await Wn(e,t)}r(id,"stageReply");async function od(e,t,n){const{source:a,wave:i,item:o}=await x(e,t);!["ready","staging"].includes(o.phase)||Ne(o)||(o.phase=n==="prototype"?"prototyping":"grilling",delete o.staged_at,await y(a.file,i))}r(od,"planAgain");async function Ms(e,t){const n=await x(e,t).catch(()=>null);!n||!["ready","staging"].includes(n.item.phase)||Ne(n.item)||(n.item.phase="grilling",delete n.item.staged_at,await y(n.source.file,n.wave),await U(e,t,{text:"A new question on it, so it is grilling until you answer."}))}r(Ms,"grillAgain");function ni(e){return e.filter(t=>t.state==="answered").map(t=>t.id)}r(ni,"answeredOnClaude");function Re(e,t,n,a=new Date().toISOString()){for(const i of e.items||[])t.includes(i.id)&&Object.assign(i,{resolved:n,resolved_at:a})}r(Re,"setResolved");async function Kt(e,t,n,a){return Re(t,n,a),await y(L(e),t),L(e)}r(Kt,"markResolved");async function sd(e,t,n){A(e);const a=await $(L(e),{topics:[],items:[]}),i=(a.items||[]).find(f=>f.id===t);i||l(`no question "${t}" in owner.json`);const{answers:o,requests:s,reopens:c}=await it(e),d=Ie(i,o,s,[],c);if(d.state==="resolved")return L(e);d.state==="requested"&&l(`${t} waits on the prototype the owner asked for; question add with round_of ${t} resolves it`);const u=S(n,"--note");Ae(i.url)&&Ws(a,i,o);const p=await Kt(e,a,[t],u);await si(e,i.topic);const w=Cn(i.topic);return w&&(await x(e,w).catch(()=>null))?.item.phase==="grilling"&&await Ya(e,w,{roundAt:!0}),p}r(sd,"resolveQuestion");async function rd(e,t,n,a=[]){A(e);const[i,...o]=[...new Set(t)];i||l("question move takes the ids of the questions to merge"),await x(e,n);const s=[n,...a];new Set(s).size!==s.length&&l("question move names a card twice");for(const f of a)await x(e,f);const c=await $(L(e),{topics:[],items:[]}),{answers:d,requests:u,reopens:p}=await it(e);for(const f of[i,...o]){const h=(c.items||[]).find(k=>k.id===f)||l(`no question "${f}" in owner.json`),{state:g}=Ie(h,d,u,[],p);g!=="open"&&l(`${f} is ${g}; question move takes open questions`)}for(const[f,h]of a.entries()){const{item:g}=await x(e,h),k=[...g.deps||[],s[f]].join(",");await pe(e,h,{deps:k,relate:n})}const w=await K(e,n);return Object.assign(c.items.find(f=>f.id===i),{topic:`item:${n}`}),Re(c,o,`Duplicate of ${w}'s question ${i}`),await y(L(e),c),L(e)}r(rd,"moveQuestions");async function cd(e,t){const n=t.filter(({choice:i,note:o})=>!i&&String(o||"").trim()).map(i=>i.id),a=n.length?await $(L(e),{topics:[],items:[]}):null;a?.items?.some(i=>n.includes(i.id))&&(Re(a,n,"Answered with a note"),await y(L(e),a))}r(cd,"resolveNoteAnswers");function Ws(e,t,n){const a=hn(t),i=e.items.filter(o=>o.topic===t.topic&&hn(o)===a).map(o=>Ie(o,n));uo(i,a)||l(`${t.id} is in prototype round ${a}, which no pick has settled yet; a pick settles it`)}r(Ws,"roundSettled");async function it(e){const t=await G(m.join(e,"data","events.jsonl"));return{answers:await Promise.all(t.filter(a=>a.kind==="owner-answer").map(async a=>{const i=await Promise.all(["html","json"].map(o=>P.stat(m.join(e,"data","replies",`${a.id}.${o}`)).then(s=>s.isFile(),()=>!1)));return{...a,reply:a.reply||i.some(Boolean)}})),requests:t.filter(a=>a.kind==="prototype-request"),reopens:t.filter(a=>a.kind===ro)}}r(it,"ownerEvents");async function dd(e,t){const{asked:n}=await Us(e),a=new Map(n.map(i=>[i.id,i.state]));return t.filter(i=>!a.has(String(i.id))||["open","requested"].includes(a.get(String(i.id))))}r(dd,"unrecorded");async function Us(e){const t=await $(L(e),{topics:[],items:[]}),{answers:n,requests:a,reopens:i}=await it(e);return{owner:t,asked:(t.items||[]).map(o=>Ie(o,n,a,[],i))}}r(Us,"topicQuestionsAll");async function ai(e,t){const n=await $(L(e),{topics:[],items:[]}),{answers:a,requests:i,reopens:o}=await it(e),s=(n.items||[]).filter(c=>c.topic===t).map(c=>Ie(c,a,i,[],o));return{owner:n,asked:s}}r(ai,"topicQuestions");const de=r((e,t)=>ai(e,`item:${t}`),"cardQuestions"),Jt=r((e,t,n)=>ii(e,`item:${t}`,n),"resolveAnswered");async function ii(e,t,n){if(!e)return[];const{owner:a,asked:i}=await ai(e,t),o=ni(i);return o.length&&await Kt(e,a,o,n),await si(e,t),o.map(s=>`resolved: ${s}`)}r(ii,"resolveAnsweredOn");const Un=r(async(e,t)=>!!e&&(await de(e,t)).asked.some(n=>n.state==="open"),"hasOpenQuestion");function oi(e,t,n){e.next=S(t,"--next"),e.next_at=new Date().toISOString(),n?e.next_on_questions=!0:delete e.next_on_questions}r(oi,"setNext");async function si(e,t){const n=t.startsWith("item:")&&!t.startsWith("item:ask:")?t.slice(5):null;if(!n)return;let a;try{a=await x(e,n)}catch(c){if(c instanceof re)return;throw c}const{source:i,wave:o,item:s}=a;s.next_on_questions&&((await de(e,n)).asked.some(c=>c.state!=="resolved")||(delete s.next,delete s.next_at,delete s.next_on_questions,await y(i.file,o)))}r(si,"clearWaitedNext");async function Hs(e,t){const{source:n,wave:a,item:i}=await x(e,t);i.next===void 0&&i.next_on_questions===void 0||(delete i.next,delete i.next_at,delete i.next_on_questions,await y(n.file,a))}r(Hs,"clearNext");async function ri(e,t,n,a){if(!e)return async()=>[];const{owner:i,asked:o}=await de(e,t),s=o.filter(d=>d.state==="open").map(d=>d.id);s.length&&!a&&l(`${n}: ${t} has ${s.length===1?"an open question":"open questions"} ${s.join(", ")} with no answer yet; pass --force to close the card and resolve ${s.length===1?"it":"them"} too`);const c=o.filter(d=>d.state!=="resolved").map(d=>d.id);return async()=>(c.length&&await Kt(e,i,c,"Closed with the card"),c.map(d=>`resolved: ${d}`))}r(ri,"closeQuestions");async function ld(e,t,n,a,i){await x(e,t),S(n,"--text"),ne(a,o=>S(o,"--owner-step")),ne(i,o=>vi(o)),await ri(A(e),t,"land",!1)}r(ld,"checkClose");async function Hn(e,t){const n=await $(L(e),{items:[]}),{answers:a,requests:i,reopens:o}=await it(e);return so((n.items||[]).map(s=>Ie(s,a,i,[],o)),t)}r(Hn,"unpickedPrototype");const zs=r(e=>e.length>120?`${e.slice(0,119)}\u2026`:e,"clip"),Ks={ask:"text","ask-edited":"text","item-message":"text","owner-answer":"note"};async function ci(e,t,n){const a=S(t,n).replace(/\s+/g," ");return(await G(m.join(e,"data","events.jsonl"))).map(o=>String(o.data?.[Ks[o.kind]]??"").replace(/\s+/g," ").toLowerCase()).some(o=>o.includes(a.toLowerCase()))||l(`${n} must quote the owner's words in an ask, card message or answer note on this board`),a}r(ci,"ownerQuote");async function ud(e,t,n){return A(e),n===void 0&&l(`only the owner's Serve / Build and the hub's Auto-build start a card run; pass --asked "<the owner's words>" quoting where the owner asked for ${t}'s run`),`Running at the owner's ask: "${await ci(e,n,"--asked")}"`}r(ud,"askedGate");async function ot(e,t,n="--from-ask"){return Pe(t,n),(await G(m.join(A(e),"data","events.jsonl"))).some(i=>i.id===t&&i.kind==="ask")?t:l(`no ask "${t}" in this site's data/events.jsonl`)}r(ot,"checkAsk");function Qt(e,t,n=new Date().toISOString(),a=di(e,t)){if(!Ke[t])throw new Error(`no lane "${t}"`);const i=e.lane;return li(e,t,a),e.lane=t,e.lane_at=n,t!=="todo"&&delete e.bumped_out,t==="done"?(e.closed_at=n,delete e.reopened_at):i==="done"&&(e.reopened_at=n),i}r(Qt,"placeIn");function di(e,t,n=!1){return t==="todo"?Ne(e)?"ready":e.phase??null:t==="progress"?"reading":t==="done"?n||Oe(e).some(a=>a.merged_at)?"merged":"closed":null}r(di,"movedPhase");function li(e,t,n){if(n!==null&&!un[t].includes(n))throw new Error(`no ${Ke[t]} phase "${n}"`);n===null?delete e.phase:e.phase=n}r(li,"setPhase");async function fd(e,t,n,a,i){const{source:o,wave:s,item:c}=await x(e,t);if(c.lane===n&&(i===void 0||(c.phase??null)===i))return null;if(c.lane===n)return li(c,n,i),await y(o.file,s),null;const d=new Date().toISOString(),u=Qt(c,n,d,i);if(await y(o.file,s),u===void 0)return null;const p={ts:d,who:"claude",kind:ua,text:`Moved to ${Ke[n]}: ${S(a,"why")}`};return await z(M(e,t),p),p.text}r(fd,"moveCard");const Y=r(e=>e?.lane==="done"||!!e?.reopened_at,"closed"),Ne=r(e=>!!e?.lane&&e.lane!=="todo","leftTodo"),wd=r(async(e,t)=>Y((await x(e,t)).item),"isFinished"),ui=["ready","grilling","prototyping"];function fi(e,t){t!==void 0&&l(`--state is now --phase: pass --phase ${ui.includes(t)?t:"ready, grilling or prototyping"}`),e==="staging"&&l("--phase staging is set by the board, on an owner action; pass ready, grilling or prototyping"),e!==void 0&&!ui.includes(e)&&l("--phase takes ready, grilling or prototyping")}r(fi,"checkPhase");const Js="a card goes ready only from staging, which the board sets on the owner's Send, Implement or Reply";async function wi(e,t){return(await G(m.join(A(e),"data","events.jsonl"))).some(a=>a.kind==="ask"&&a.id===t&&a.data?.quick===!0||a.kind==="item-message"&&a.data?.key===`ask:${t}`&&a.data?.intent==="build")}r(wi,"quickAsk");const Qs=/^mark (?:it |this |the card )?ready\b/i;function Gs(e,t,n){const a=[].concat(t).map(s=>xn(s)||l(`--pr takes a PR number or owner/repo#<n>, not "${s}"`)),i=r((s,c)=>s.number===c.number&&(s.repo||n||null)===(c.repo||n||null),"same"),o=Oe(e);for(const s of a)o.some(c=>i(c,s))||o.push(s);delete e.pr,e.prs=o}r(Gs,"addPRs");const J=r((e,t)=>String(t).includes("/")?String(t):`${e}/${t}`,"depKey");async function st(e,t,n){const a=(n.deps||[]).map(o=>J(t,o)),i=await Promise.all(a.map(o=>x(e,o).then(({item:s})=>s,()=>null)));return a.filter((o,s)=>i[s]&&!Y(i[s]))}r(st,"unfinishedDeps");async function Ys(e,t,n){const a=await Promise.all((await st(e,I(t).wave,n)).map(i=>K(e,i)));return n.blocked_until?[...a,n.blocked_until]:a}r(Ys,"blockerNames");async function Gt(e,t,n){n??=(await x(e,t)).item;const a=await Promise.all((await st(e,I(t).wave,n)).map(i=>K(e,i)));return[a.length&&`by ${a.join(", ")}`,n.blocked_until&&`until ${n.blocked_until}`].filter(Boolean).join(" and ")}r(Gt,"blockWords");const Zs=r((e,t)=>!Ce(e)&&!e.pushed&&!e.merge_ready&&!Y(e)&&(!!fn(t)||t.some(n=>n?.started&&!n.done&&Je(n))),"inProgress");async function Vs(e,t){const n=[];for(const a of await F(e)){const i=await $(a.file,null);for(const o of Array.isArray(i?.items)?i.items:[])Y(o)||o.blocked_until||!(o.deps||[]).some(s=>J(a.slug,s)===t)||(await st(e,a.slug,o)).length||n.push(`${a.slug}/${o.id}`)}return n}r(Vs,"unblockedBy");async function pi(e,t,n){const{source:a,wave:i,item:o}=await x(e,t);Ne(o)&&(o.unblocked_at=n,await y(a.file,i))}r(pi,"markUnblocked");async function hi(e,t,n,a){const i=new Set(a.match(/#?[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),o=new Set((n.deps||[]).map(s=>J(I(t).wave,s)));for(const s of await F(e)){const c=await $(s.file,null);for(const d of c?.items||[]){const u=`${s.slug}/${d.id}`;if(u===t||o.has(u)||Y(d))continue;[d.id,Qe(c,d),d.ticket].filter(Boolean).flatMap(w=>[w,`${s.slug}/${w}`]).some(w=>i.has(w))&&l(`--next names unfinished card ${u}; add it with --deps`)}}}r(hi,"checkNextDeps");const mi=r(e=>`item notes ${e} --heading "<heading>" --file <draft.md>`,"NOTES_COMMAND");function Xs(e,t){t.size||l(`${e} has no size; a builder starts from it, so pass --size small or large with --phase ready`),t.plan||l(`${e} has no notes file; a builder starts from it, so add its notes with ${mi(e)} before --phase ready`)}r(Xs,"checkSizeAndNotes");async function er(e,t,n,a){Xs(t,a);const i=await P.readFile(await Yt(n,a.plan),"utf8"),o=(await de(e,t)).asked.filter(s=>s.answers.length&&!i.includes(s.id)&&!(s.prototype_gate&&!s.answers.at(-1).note)).map(s=>s.id);o.length&&l(`${t}'s notes leave out the answered question${o.length===1?"":"s"} ${o.join(", ")}; record each answer in its notes, naming its id, with ${mi(t)} before it is ready`)}r(er,"checkNotesComplete");async function Yt(e,t,{missingOk:n=!1}={}){const a=r(s=>l(`a notes file is a .md file inside ${e}, named relative to it: "${t}" ${s}`),"bad");(typeof t!="string"||!t.trim())&&a("is empty"),(m.isAbsolute(t)||t.split(/[\\/]/).includes(".."))&&a("leaves the folder"),m.extname(t).toLowerCase()!==".md"&&a("is not a .md file");const i=await P.realpath(e),o=await P.realpath(m.resolve(e,t)).catch(()=>n?m.resolve(i,t):a("does not exist"));return o.startsWith(i+m.sep)||a("leaves the folder"),o}r(Yt,"planPath");async function tr(e,t){const{source:n,wave:a,item:i}=await x(e,t);if(i.plan)return{wave:a,item:i,markdown:await P.readFile(await Yt(n.dir,i.plan),"utf8")};if(typeof i.notes=="string"&&i.notes.trim())return{wave:a,item:i,markdown:i.notes};l(`${t} has no notes`)}r(tr,"readNotes");function gi({install:e,installSha:t,installLabel:n,clearInstall:a}){if(e===void 0)return(t!==void 0||n!==void 0)&&l("--install-sha and --install-label go with --install <url>"),a?null:void 0;a&&l("pass --install or --clear-install, not both");let i;try{i=new URL(String(e).trim())}catch{i=null}(!i||!["https:","itms-services:"].includes(i.protocol))&&l(`--install takes an https:// or itms-services:// URL, not "${e}"`),i.protocol==="itms-services:"&&!(i.searchParams.get("action")==="download-manifest"&&/^https:\/\//.test(i.searchParams.get("url")||""))&&l("--install: an itms-services:// URL needs ?action=download-manifest&url=https://\u2026/manifest.plist");const o=t===void 0?void 0:/^[0-9a-f]{7,40}$/i.test(t)?t.toLowerCase():l(`--install-sha takes a commit sha of 7 to 40 hex digits, not "${t}"`);return{url:String(e).trim(),...n===void 0?{}:{label:S(n,"--install-label")},built_at:new Date().toISOString(),...o?{sha:o}:{}}}r(gi,"installOf");async function pd(e,{install:t,installSha:n}){const a=gi({install:t,installSha:n});a||l("main set needs --install <url>");const i=m.join(A(e),"data","main.json");return await y(i,{install:a}),{file:i,lines:["main install set"]}}r(pd,"setMainInstall");const $i=r((e,t)=>e.length?`Merged PR${e.length>1?"s":""} ${Ge(e,t)}.`:"Merged.","mergedLine");function nr(e,t,n,a,i){return n?a??$i(n,i):(a===void 0&&e&&l(`--done needs --text "<what was done, or why it closes without landing>", which becomes ${t}'s last thread line`),a??null)}r(nr,"closingLine");function ar(e,t,n){const a=new Date().toISOString(),i=r((o,s)=>o.number===s.number&&(o.repo||n||null)===(s.repo||n||null),"same");e.prs=Oe(e).map(o=>{const s=t.find(c=>i(o,c));return s&&!o.merged_at?{...o,merged_at:s.at||a}:o}),delete e.pr}r(ar,"markMerged");async function ki(e,t,{ticket:n,status:a,clearStatus:i,done:o,merged:s,landed:c,start:d,handBack:u,next:p,clearNext:w,pr:f,pushed:h,mergeReady:g,ghSince:k,fromAsk:v,phase:D,state:O,held:B,unheld:N,pickUpAt:_,deps:E,blockedUntil:C,blockReason:T,parent:W,related:j,relate:Q,unrelate:ae,install:Z,installSha:ee,installLabel:ye,clearInstall:Ot,title:rt,size:le,model:xe,force:Pt,text:ce,who:Dt,by:an,command:te,ownerStep:ve,ownerDone:ue,check:ct,stopRun:Ji},Vn){(a!==void 0||i)&&l(`--status${i?" and --clear-status are":" is"} gone: a card's lane is stored and moves itself. Close it with --done --text "<what was done, or why it closes without landing>"`),s&&(o=!0),j!==void 0&&(Q!==void 0||ae!==void 0)&&l("pass --related, or --relate and --unrelate, not both"),fi(D,O),Pt&&!o&&l("--force goes with --done"),v!==void 0&&o&&l("--from-ask opens a card; close it in another call"),ce!==void 0&&S(ce,"--text"),ve!==void 0&&S(ve,"--owner-step");const dt=ct===void 0||ct===""?ct:vi(ct);ce!==void 0&&v===void 0&&!o&&D!=="ready"&&l("--text goes with --from-ask, --done, or --phase ready on a staging card"),v!==void 0&&ce===void 0&&l(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),p!==void 0&&w&&l("pass --next or --clear-next, not both"),typeof p=="string"&&!p.trim()&&([p,w]=[void 0,!0]);const lt=yi({held:B,unheld:N,pickUpAt:_}),At=gi({install:Z,installSha:ee,installLabel:ye,clearInstall:Ot});[o,d,u,p,w,f,h,g,k,v,D,lt,E,C,T,W,j,Q,ae,At,rt,le,xe,n,ve,ct].every(R=>R===void 0||R===!1)&&l("nothing to set: pass --done, --next, --clear-next, --pr, --pushed, --gh-since, --from-ask, --phase, --held, --unheld, --pick-up-at, --deps, --blocked-until, --block-reason, --parent, --related, --relate, --unrelate, --install, --clear-install, --title, --size, --model, --ticket or --check");const V=v===void 0?void 0:await ot(e,v);V&&await Mt(e,V);const Qi=V!==void 0&&await wi(e,V),{source:_e,wave:Ue,item:b}=await x(e,t);D!==void 0&&Ne(b)&&l(`--phase sets a Todo card's phase; ${t} is ${Ke[b.lane]}, where the board sets it`),D==="ready"&&!Qi&&!["staging","ready"].includes(b.phase)&&l(`${t} reads ${b.phase||"new"}, not staging; ${Js}`),V&&b.from_ask&&b.from_ask!==V&&l(`${t} came from ask "${b.from_ask}", so ask "${V}" can't be its from-ask; to fold it in: item relate-ask ${V} ${t} --summary "<line>"`),V&<&&D!==void 0&&!b.phase&&l("a card filed held is filed New and takes no --phase; drop --phase, or drop --held to file it in a phase"),V&&D===void 0&&!b.phase&&!lt&&l("--from-ask files a card, which reads New until it has a phase: pass --phase grilling, prototyping, or ready for a Quick build ask, or --held to file it for later");const Xn=Y(b);ve!==void 0&&Xn&&l(`${t} is already Done, so its ticket closed; an owner step goes on the command that finishes the card: land --owner-step, or builder done before the merge`),ve!==void 0&&(b.owner_step=S(ve,"--owner-step")),ue&&delete b.owner_step,dt!==void 0&&!b.owner_step&&l(`--check goes with an owner step, which ${t} doesn't have: pass --owner-step "<the step>" with it on builder done or land`),dt&&(b.owner_check={check:dt,since:b.owner_check?.check===dt?b.owner_check.since:new Date().toISOString()}),(dt===""||!b.owner_step)&&delete b.owner_check;const Rt=await Promise.all((await H(_e.dir,b.id)).map(R=>$(R,null))),Gi=pn(b,Rt);if(rt!==void 0&&(b.title=S(rt,"--title")),bi(b,{size:le,model:xe}),E!==void 0){const R=await _i(e,I(t).wave,E);R.length?b.deps=R:delete b.deps}const ea=C===""?b.blocked_until:void 0;if(C!==void 0&&(C===""?delete b.blocked_until:b.blocked_until=S(C,"--blocked-until")),T&&(!b.deps?.length&&!b.blocked_until&&l("--block-reason goes with a block: --deps or --blocked-until"),b.block_reason=S(T,"--block-reason")),(T===""||!b.deps?.length&&!b.blocked_until)&&delete b.block_reason,W!==void 0){const R=await Si(e,I(t).wave,t,W);R?b.parent=R:delete b.parent}const ta=W!==void 0||j!==void 0||Q!==void 0||ae!==void 0?await zn(e,t):null;let on=j===void 0?null:await sr(e,t,b,j);Q!==void 0&&(b.related=[...new Set([...(b.related||[]).map(R=>J(I(t).wave,R)),...await Vt(e,t,Q,"--relate")])]),ae!==void 0&&(on=await rr(e,t,b,await Vt(e,t,ae,"--unrelate"))),lt!==void 0&&Zt(b,lt),d&&b.held&&l(b.pick_up_at?`${t} is held until ${b.pick_up_at}, when the board resumes it; start it early only with item set ${t} --unheld`:`${t} is held; start it only once the owner resumes it, with item set ${t} --unheld`);const ut=f!==void 0||h!==void 0||g!==void 0;if((d||o||u||ut)&&or(b),d){const R=await st(e,I(t).wave,b);R.length&&l(`${t} is blocked by ${R.join(", ")}; start it once ${R.length===1?"that is":"those are"} done`),b.blocked_until&&l(`${t} is blocked until ${b.blocked_until}; start it once that holds and the block is cleared with --blocked-until ""`)}const Nt=d&&e?await Hn(e,t):null;Nt&&l(`${t} has no take picked: the owner's answer to ${Nt.id} picks none${Nt.note?` ("${zs(Nt.note)}")`:""}, so it prototypes again: draw the next round from that note, or grill when the note asks a question, and build once a take is picked`);const Yi=o?await ri(e,t,"--done",Pt):async()=>[],na=o?nr(e,t,s,ce,oe(e,Ue)):null;if(s&&ar(b,s,oe(e,Ue)),p!==void 0&&(oi(b,p,await Un(e,t)),await hi(e,t,b,b.next)),w&&(delete b.next,delete b.next_at,delete b.next_on_questions),d&&p===void 0&&b.next_on_questions&&(delete b.next,delete b.next_at,delete b.next_on_questions),ut&&!b.review_since&&(b.review_since=new Date().toISOString()),f!==void 0&&Gs(b,f,oe(e,Ue)),h!==void 0){ge().find(R=>je(R.dir,A(e)))?.remote||l("--pushed is for a board whose repo is a plain git remote; on GitHub, open a PR and pass --pr"),Ce(b)&&l(`${t} has a PR; its PR says when it merges`);try{Do(h)}catch{l("--pushed takes the name of the branch you pushed")}b.pushed={branch:h,at:new Date().toISOString()},b.branch=h}if(g!==void 0&&(b.merge_ready={branch:g,at:new Date().toISOString()},b.branch=g),o&&delete b.merge_ready,k!==void 0&&((typeof k!="string"||!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(k)||Number.isNaN(Date.parse(k)))&&l("--gh-since takes an ISO-8601 UTC time such as 2026-10-23T11:00:00Z"),b.gh_since=k),V&&(b.from_ask=V),D==="ready"&&e){const R=(await de(e,t)).asked.filter(ze=>ze.state==="open").map(ze=>ze.id);R.length&&l(`${t} has ${R.length===1?"an open question":"open questions"} ${R.join(", ")}; it is ready once the owner has answered ${R.length===1?"it":"them"}`),await er(e,t,_e.dir,b)}const sn=D==="ready"&&b.phase==="staging"&&v===void 0;sn&&ce===void 0&&l(`${t} goes ready from staging on your judgment: pass --text "<what you checked it against on the board>", which goes into its notes`),D==="ready"&&!sn&&ce!==void 0&&v===void 0&&!o&&l(`${t} reads ${b.phase||"no phase"}, not staging, so there is no judgment for --text to record`);const Zi=D==="ready"&&b.phase!=="ready";D!==void 0&&(D!=="ready"&&["staging","ready"].includes(b.phase)&&(b.round_at=new Date().toISOString()),b.phase=D,delete b.staged_at,delete b.unfiled,delete b.filed_held),b.phase==="ready"&&Qs.test(b.next??"")&&l(`${t} would read ready with Next "${b.next}", which asks for the phase it is already in; pass --next with what happens next, or --clear-next`),At&&(b.install=At),At===null&&delete b.install,!Gi&&!d&&pn(b,Rt)&&l(`that leaves ${t} In Progress with nothing building and no Next; pass --next "<what happens next>"`);const He=(E!==void 0||C)&&!o&&Zs(b,Rt)?await Gt(e,t,b):"",rn=Rt.some(R=>R?.started&&!R.done&&Je(R)),Vi=He&&rn?await Ji():[],aa=oe(e,Ue);let cn=null;const Et=r((R,ze)=>{b.lane!==R&&Qt(b,R,void 0,di(b,R,c))!==void 0&&ze&&(cn=`Moved to ${Ke[R]}: ${ze}`)},"moveTo");o?Et("done",null):He&&rn?Et("todo",null):ut&&b.lane!=="done"?Et("review",f!==void 0?`PR ${Ge([].concat(f).map(xn),aa)} opened`:h!==void 0?`${h} pushed for its merge`:"verified, it waits for your Merge"):d&&!Y(b)&&Et("progress","a build started"),(n!==void 0||V&&!b.ticket&&!b.linear?.length)&&await Kn(e,t,Ue,b,n,Vn,_e.dir),await y(_e.file,Ue),cn&&await z(M(e,t),{ts:b.lane_at,who:"claude",kind:ua,text:cn}),sn&&await at(e,t,`Ready (orchestrator, ${yt()})`,ce),Zi&&await z(M(e,t),{ts:new Date().toISOString(),who:"claude",command:"readied",text:he.readied}),He&&rn?(await nn(_e.dir,b.id,!1,{endHandBack:!0}),await z(M(e,t),{ts:new Date().toISOString(),who:"claude",text:`Blocked ${He}: back to Todo. It starts again once nothing blocks it.`})):He&&await z(M(e,t),{ts:new Date().toISOString(),who:"claude",text:`Blocked ${He}: it stays In Progress.`}),on&&await on(),ta&&await Oi(e,t,ta,await zn(e,t));const Se=[...Vi,...await Yi()];D==="ready"&&e&&Se.push(...await Jt(e,t,"Recorded in the card's notes")),d&&Se.push(...await Jt(e,t,"Acted on: build started")),V&&Se.push(...(await U(e,t,{text:ce})).lines),na&&e&&Se.push(...(await U(e,t,{text:na,who:Dt,by:an,command:te})).lines);const ia=b.owner_step&&o?null:xo({start:d,done:o,handover:ut}),oa=ia&&await vo(await Pi(e),b,ia,{...Vn,repo:aa});oa&&Se.push(oa),(ut||o||u)&&(await nn(_e.dir,b.id,o,u?{handedBack:new Date().toISOString()}:{endHandBack:!0}),await nt(e,t));const dn=V?await De(e):{};if(X(dn,V)!==void 0&&(delete dn[V],await y(ke(e),{links:dn})),ea&&!Y(b)&&!(await st(e,I(t).wave,b)).length){const R=new Date().toISOString();await z(M(e,t),{ts:R,who:"claude",text:`Unblocked: ${ea}.`}),await pi(e,t,R),await me({dir:e},"card-unblocked",{key:t}),Se.unshift(`unblocked: ${t}`)}if(!o||Xn)return{file:_e.file,lines:Se};const sa=await Vs(e,t),ra=new Date().toISOString();for(const R of sa)await z(M(e,R),{ts:ra,who:"claude",text:`Unblocked: ${await K(e,t)} is done.`}),await pi(e,R,ra),await me({dir:e},"card-unblocked",{key:R});return{file:_e.file,lines:[...sa.map(R=>`unblocked: ${R}`),...Se]}}r(ki,"changeItem");function bi(e,{size:t,model:n}){for(const[a,i,o]of[["size",t,["small","large"]],["model",n,["opus","sonnet"]]])i!==void 0&&(i===""?delete e[a]:e[a]=o.includes(i)?i:l(`--${a} takes ${o.join(" or ")}, or "" to clear it`))}r(bi,"setBuildChoice");function yi({held:e,unheld:t,pickUpAt:n}){return[e,t,n!==void 0].filter(Boolean).length>1&&l("pass one of --held, --unheld and --pick-up-at"),n!==void 0?n===""?null:{until:ir(n)}:e?{until:null}:t?null:void 0}r(yi,"holdOf");function ir(e){const t=typeof e=="string"&&bo.test(e)?Date.parse(e):NaN;return Number.isNaN(t)&&l('--pick-up-at takes an ISO time such as 2026-11-04T09:00, the host\u2019s local time unless it names an offset, or "" to resume the card'),t<=Date.now()&&l(`--pick-up-at ${e} has passed; pass a later time, or --unheld to resume the card now`),new Date(t).toISOString()}r(ir,"pickUpTime");function or(e){delete e.held,delete e.pick_up_at}r(or,"clearHold");function Zt(e,t){t?e.held=!0:delete e.held,t?.until?e.pick_up_at=t.until:delete e.pick_up_at,e.held_at=new Date().toISOString()}r(Zt,"setHold");async function hd(e,t,n,{announce:a=!0}={}){const{source:i,wave:o,item:s}=await x(e,t);if(a){const c=new Date(n).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});await z(M(e,t),{ts:new Date().toISOString(),who:"claude",text:`Resumed: its hold until ${c} ended.`}),!s.blocked_until&&!(await st(e,I(t).wave,s)).length&&await me({dir:e},"card-unblocked",{key:t,until:n})}Zt(s,null),await y(i.file,o)}r(hd,"pickUp");async function xi(e,t,n){const a=String(n?.key||""),{wave:i,id:o}=I(a),{source:s,wave:c}=await Te(e,i),d=n.shared_by?n.shared_by.name||n.shared_by.login:void 0,u={text:he.done,who:"love",by:d,command:"done"},p=c.items.find(g=>g.id===o)||l(`wave "${i}" has no item "${o}"`);if(!(t==="card-done"&&!!p.owner_step)&&p.lane==="done")return null;if(t==="card-done")return pe(e,a,{done:!0,force:!0,ownerDone:!0,...u});const f=n.hold?typeof n.until=="string"?new Date(n.until).toISOString():null:void 0;Zt(p,n.hold?{until:f}:null),await y(s.file,c),n.hold&&(await nn(s.dir,o,!1,{held:!0}),await nt(e,a));const h=f&&new Date(f).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});return U(e,a,n.hold?{text:h?`${he.hold} until ${h}`:he.hold,who:"love",by:d,command:"hold"}:{text:he.resume,who:"love",by:d,command:"resume"})}r(xi,"recordOwnerTap");function vi(e){try{return Ho(Uo(e))}catch(t){return l(t.message)}}r(vi,"checkSpec");async function md(e,t,n,a){const{item:i}=await x(e,t);return!i.owner_step||i.owner_check?.check!==n?null:pe(e,t,{done:!0,force:!0,ownerDone:!0,text:`Check passed: ${a}. Closed.`})}r(md,"passOwnerCheck");async function gd(e,t,n,a){const{source:i,wave:o,item:s}=await x(e,t);return!s.owner_step||s.owner_check?.check!==n?!1:(s.owner_check.broken=a,await y(i.file,o),await U(e,t,{text:`The check for "${s.owner_step}" can't run (${a}), so it stopped; Claude is asked to fix it. Done still closes the card.`}),await me({dir:e},"owner-check-broken",{key:t,check:n,error:a}),!0)}r(gd,"breakOwnerCheck");async function _i(e,t,n){const a=[...new Set(String(n).split(",").map(i=>i.trim()).filter(Boolean))];for(const i of a)await x(e,J(t,i));return a}r(_i,"depList");async function Si(e,t,n,a){if(!String(a).trim())return null;const i=J(t,String(a).trim());i===n&&l("--parent: a card can't be its own Parent");for(let o=i;o;){const s=o===i?await x(e,o):await x(e,o).catch(d=>{if(d instanceof re)return null;throw d});if(!s)break;const{item:c}=s;c.parent&&J(I(o).wave,c.parent)===n&&l(`--parent: ${i} is a child of ${n}, so it can't be its Parent`),o=c.parent?J(I(o).wave,c.parent):null}return i}r(Si,"parentKey");async function Vt(e,t,n,a){const i=t.split("/")[0],o=[...new Set(String(n).split(",").map(s=>s.trim()).filter(Boolean).map(s=>J(i,s)))];o.includes(t)&&l(`${a}: a card can't be related to itself`);for(const s of o)await x(e,s);return o}r(Vt,"relatedKeys");const ji=r((e,t,n)=>async()=>{for(const a of await F(e)){const i=await $(a.file,null);let o=!1;for(const s of Array.isArray(i?.items)?i.items:[]){const c=`${a.slug}/${s.id}`;c===t||!n(c)||!(s.related||[]).some(d=>J(a.slug,d)===t)||(s.related=s.related.filter(d=>J(a.slug,d)!==t),s.related.length||delete s.related,o=!0)}o&&await y(a.file,i)}},"dropLinksTo");async function zn(e,t){const n=new Set;let a=null;for(const i of await F(e))for(const o of(await $(i.file,null))?.items||[]){const s=`${i.slug}/${o.id}`,c=(o.related||[]).map(d=>J(i.slug,d));s===t?(c.forEach(d=>n.add(d)),o.parent&&(a=J(i.slug,o.parent))):c.includes(t)&&n.add(s)}return{related:n,parent:a}}r(zn,"linksOf");async function Oi(e,t,n,a){const i=r(async(o,s,c=o,d=o)=>{await U(e,t,{text:he[c],command:c,card:s}),await U(e,s,{text:he[d],command:d,card:t})},"both");for(const o of a.related)n.related.has(o)||await i("related",o);for(const o of n.related)a.related.has(o)||await i("unrelated",o);a.parent&&a.parent!==n.parent&&await i("followup-of",a.parent,"followup-of","followup")}r(Oi,"postLinkLines");async function sr(e,t,n,a){const i=await Vt(e,t,a,"--related");return i.length?n.related=i:delete n.related,ji(e,t,o=>!i.includes(o))}r(sr,"setRelated");function rr(e,t,n,a){const i=(n.related||[]).filter(o=>!a.includes(J(I(t).wave,o)));return i.length?n.related=i:delete n.related,ji(e,t,o=>a.includes(o))}r(rr,"unrelateCards");const oe=r((e,t)=>e&&ge().find(n=>je(n.dir,e))?.github||t.repo,"boardRepoOf");async function $d(e,t){const{wave:n,item:a}=await x(e,t);Ce(a)||l(`${t} has no PR`);const i=Le(a,oe(e,n));i.length||l(`${t} has no repo`);const o=a.gh_since||"1970-01-01T00:00:00Z";try{const s=be(await ie(e)).github_login||await Io();return await Promise.all(i.map(async({repo:c,number:d})=>({repo:c,...ha(await pa({repo:c,pr:d,since:o}),{pr:d,since:o,me:s})})))}catch(s){l(s.message)}}r($d,"prReport");async function kd(e,t,{replyTo:n,body:a}){const i=n===void 0?"pr comment":"pr reply",o=xn(t)||l(`${i} takes a PR number or owner/repo#<n>`);n!==void 0&&!/^[1-9]\d*$/.test(n)&&l("pr reply takes the id of the review comment it answers");const s=S(a,"--body").trim();(s.startsWith(pt.trim())||s.includes(yn))&&l(`${i} adds the ${pt.trim()} prefix and the claude-bridge marker itself; pass only your words`);const c=o.repo||oe(A(e),{})||l(`${i} needs a board on GitHub; this board has no repo there`);try{const d=await Bo({repo:c,pr:o.number,replyTo:n,words:s,login:be(await ie(e)).github_login}),u=await Ca(e,ht({repo:c,number:o.number}),"comment",`Comment on #${o.number}: ${s}`);return{lines:[`posted on PR ${o.repo?t:`#${t}`}: ${d}`,...u.length?[`queued for ${u.join(", ")}'s Slack thread`]:[]]}}catch(d){l(d.message)}}r(kd,"postPR");const Pi=r(async e=>Ua((await ie(e)).tickets),"boardSource");async function bd(e,t,{afterApproval:n,confirmed:a,tapped:i}={}){A(e);const{wave:o,item:s}=await x(e,t);Ce(s)||l(`${t} has no PR`),Y(s)&&l(`${t} is done`);const c=await Gt(e,t,s);c&&(i&&await U(e,t,{text:`Not merged: blocked ${c}.`}),l(`Not merged: blocked ${c}.`));const d=oe(e,o),u=Le(s,d);u.length||l(`${t} has no repo`);const p=new Date().toISOString();let w;try{w=await Promise.all(u.map(_=>pa({repo:_.repo,pr:_.number,since:p})))}catch(_){l(_.message)}const f=w.map(({view:_})=>_);if(To(f)){const _=u.map((E,C)=>({...E,at:f[C].mergedAt})).filter((E,C)=>f[C].state==="MERGED");return pe(e,t,{merged:_,text:`PR${_.length>1?"s":""} ${Ge(_,d)} ${_.length>1?"were":"was"} already merged.`})}const h=u.map((_,E)=>({..._,...w[E]})).filter(({view:_})=>_.state==="OPEN"),g=_n(o,(await ie(e)).reviewers||[]).length>0,k=[];let v=!1,D=!1;for(const _ of h){const{view:E,reviews:C}=_,{failing:T,pending:W}=ha(_,{pr:_.number,since:p,me:null}),j=oo({...E,checks:{state:T.length?"fail":W?"pending":"pass",failing:T,pending:W}},{needsApproval:g}),Q=C.filter(Z=>Z.state==="APPROVED").sort((Z,ee)=>String(Z.submitted_at).localeCompare(String(ee.submitted_at))).at(-1)?.commit_id;let ae=!!(Q&&E.headRefOid&&Q!==E.headRefOid);ae&&n===void 0&&$r(s,Q,E.headRefOid)&&(ae=!1,D=!0),v||=ae,ae&&n===void 0&&j.push('commits after the approval: pass --after-approval "<why they are trivial>" when they are, else ask the approver to look again'),k.push(...u.length>1?j.map(Z=>`${Ge([_],d)} ${Z}`):j)}h.length||k.push(u.length>1?"every PR is closed":"PR is closed"),n!==void 0&&!v&&l("--after-approval: no commit came after the approval; drop it");const O=s.manual_steps?.length?a===void 0?null:await ci(e,a,"--confirmed"):void 0;if(a!==void 0&&O===void 0&&l(`--confirmed: ${t} has no manual steps; drop it`),O===null&&k.push(`manual steps: merge only on the owner's word, with --confirmed "<their words>"`),k.length){const _=`Not merged: ${k.join("; ")}.`;i&&await U(e,t,{text:_}),l(_)}const B=[];for(const _ of h){const E=await new Promise(C=>{ca("gh",["pr","merge",String(_.number),"--repo",_.repo,"--squash"],{encoding:"utf8",timeout:12e4},(T,W,j)=>C(T?String(j||T.message).trim().split(`
|
|
25
|
+
`)[0]:null))});E&&l(`gh pr merge ${Ge([_],d)} failed: ${E}${B.length?` (merged ${Ge(B,d)} before it)`:""}`),B.push(_)}const N=[$i(B,d),n!==void 0&&`After the approval: ${S(n,"--after-approval")}`,D&&`After the approval: only the board's rebase onto ${s.rebased.at(-1).base}.`,O&&`The owner's word on the manual steps: "${O}"`];return pe(e,t,{merged:B,text:N.filter(Boolean).join(" ")})}r(bd,"mergeCard");async function yd(e,t){return(await x(e,t)).item.merge_ready||null}r(yd,"heldMerge");async function xd(e,t){const{source:n,wave:a,item:i}=await x(e,t);(!i.merge_ready||Y(i))&&l(`${t} isn't waiting for its Merge`);const o=await Gt(e,t,i);return o&&l(`${t} is blocked ${o}, so its Merge waits until nothing blocks it`),i.merge_ready.tapped=new Date().toISOString(),await y(n.file,a),i.merge_ready}r(xd,"tapMerge");async function vd(e,t){const{source:n,item:a}=await x(e,t);return(await Promise.all((await H(n.dir,a.id)).map(o=>$(o,null)))).filter(o=>o?.worktree?.path).sort((o,s)=>String(s.started).localeCompare(String(o.started)))[0]?.model}r(vd,"builderModel");async function Kn(e,t,n,a,i,o,s,c){try{const d=await ie(e),u=kn(d.tickets||{source:"internal"}),p=r((h,g)=>h.internal===!0?String(g.id).replace(/^S(\d+)$/,`${u.prefix}-$1`):null,"ownId");i===void 0&&u.source==="internal"&&bn(u,p(n,a))&&(i=p(n,a));let w=0;if(u.source==="internal")for(const h of await F(e)){const g=await $(h.file,{});for(const k of g.items||[]){const v=[k.ticket,p(g,k)];if(w=Math.max(w,...v.map(D=>bn(u,D)||0)),i!==void 0&&`${h.slug}/${k.id}`!==t&&v.includes(i))throw Object.assign(new re(`${i} is already the ticket of ${h.slug}/${k.id}`),{held:!0})}}c??=i===void 0&&u.source!=="internal"&&a.plan?await P.readFile(await Yt(s,a.plan),"utf8"):a.notes||"";const f=await yo(u,{ticket:i,title:a.title,body:c,repo:oe(e,n),next:w+1},o);a.ticket=f.ticket,f.created&&u.source!=="internal"?a.ticket_created=!0:delete a.ticket_created}catch(d){if(d instanceof re)throw d;l(d.message)}}r(Kn,"assignTicket");const Ee=r((e,t)=>wt(m.join(e||Ct(),"tickets"),t,{heartbeat:!0}),"ticketWrite"),pe=r((e,t,n,a)=>n.ticket!==void 0||n.fromAsk!==void 0?Ee(e,()=>ki(e,t,n,a)):ki(e,t,n,a),"setItem"),_d=r((e,t,n,a)=>Ee(e,()=>cr(e,t,n,a)),"addItem");async function cr(e,t,{title:n,notes:a,notesFrom:i,deps:o,ticket:s,pickUpAt:c,held:d,parent:u,size:p,model:w,phase:f,state:h,relate:g,fromAsk:k,text:v,intake:D=!1},O){const{wave:B,id:N}=t.includes("/")?I(t):{wave:t};N!==void 0&&Pe(N,"the item id"),fi(f,h),v!==void 0&&S(v,"--text"),v!==void 0&&k===void 0&&l("--text goes with --from-ask"),k!==void 0&&v===void 0&&l(`--from-ask needs --text "<first line>", the item's first thread line after the ask`);const _=k===void 0?void 0:await ot(e,k);_&&await Mt(e,_);const E=yi({held:d,pickUpAt:c});E&&f!==void 0&&l("a card filed held is filed New and takes no --phase; drop --phase, or drop --held to file it in a phase"),_&&f===void 0&&!E&&l("--from-ask files a card, which reads New until it has a phase: pass --phase grilling, prototyping, or ready for a Quick build ask, or --held to file it for later"),f==="ready"&&!(_&&await wi(e,_))&&l(_?`ask "${_}" is not a Quick build ask, so item add files it grilling or prototyping`:"item add files a card grilling or prototyping; ready is for a Quick build ask, filed with --from-ask"),f==="ready"&&!p&&l("a new ready card has no size; a builder starts from it, so pass --size small or large with --phase ready"),f==="ready"&&!(a??"").trim()&&i===void 0&&l("a new ready card has no notes; a builder starts from them, so pass --notes or --notes-from with --phase ready"),D&&e&&f===void 0&&!E&&l("item add files a card on a board: pass --phase grilling or prototyping, or --held to file it for later");const{source:C,wave:T}=await Te(e,B);N!==void 0&&T.items.some(ee=>ee.id===N)&&l(`wave "${B}" already has an item "${N}"`);const W=new Date().toISOString(),j={id:N,title:S(n,"--title"),created:W,lane:"todo",lane_at:W};u!==void 0&&(j.parent=await Si(e,B,null,u)||l("--parent takes a card, such as <wave>/<id>")),a!==void 0&&i!==void 0&&l("pass --notes or --notes-from, not both");const Q=i!==void 0?await ei(e,i,"--notes-from",()=>`${Mn}, and link that card with --relate`):a===void 0?"":S(a,"--notes");i!==void 0&&!Q.trim()&&l(`--notes-from ${i} is empty`);const ae=i!==void 0?Q:`# ${j.title}
|
|
26
|
+
${Q&&`
|
|
27
|
+
${Q}
|
|
28
|
+
`}`;bi(j,{size:p,model:w}),o!==void 0&&(j.deps=await _i(e,B,o)),E&&Zt(j,E),E&&f===void 0&&(j.filed_held=!0),g!==void 0&&(j.related=await Vt(e,t,g,"--relate")),_&&(j.from_ask=_),f!==void 0&&(j.phase=f),await Kn(e,t,T,j,s,O,C.dir,Q),N===void 0&&(j.id=So(j.ticket),T.items.some(ee=>ee.id===j.id)&&l(`wave "${B}" already has an item "${j.id}"; link ${j.ticket} with item add ${B}/<id> --ticket ${j.ticket}`)),j.plan=qn(C.dir,j.id),await Fn(m.join(C.dir,j.plan),ae);const Z=`${B}/${j.id}`;if(T.items.push(j),await y(C.file,T),_){await U(e,Z,{text:v});const ee=await De(e);X(ee,_)!==void 0&&(delete ee[_],await y(ke(e),{links:ee}))}return e&&await Oi(e,Z,{related:new Set,parent:null},await zn(e,Z)),{file:C.file,key:Z,lines:[]}}r(cr,"createItem");const Sd=r((e,{dryRun:t=!1}={},n)=>Ee(A(e),()=>dr(e,t,n)),"backfillTickets");async function dr(e,t,n){const a=await Pi(e)||l("this board has no ticket source yet; pick one in Board settings first"),i=new Set,o=[];for(const p of await F(e)){const w=await $(p.file,null);if(Array.isArray(w?.items)){o.push({entry:p,wave:w});for(const f of w.items)f.ticket&&i.add(f.ticket),w.internal===!0&&a.source==="internal"&&i.add(String(f.id).replace(/^S(\d+)$/,`${a.prefix}-$1`))}}const s=[],c={linked:0,created:0,failed:0},d=r((p,w)=>w.ticket&&!(a.source!=="internal"&&p.internal===!0&&w.ticket===w.id),"ticketed");for(const{entry:p,wave:w}of o)for(const f of w.items){if(d(w,f)||Y(f))continue;const h=`${p.slug}/${f.id}`,g=[..._o(a,f.title),...(f.linear||[]).filter(k=>bn(a,k))].find(k=>a.source!=="internal"||!i.has(k));if(t){c[g?"linked":"created"]++,s.push(`${h} would ${g?`link ${g}`:"get a new ticket"}`);continue}try{await Kn(e,h,w,f,g,n,p.dir),await y(p.file,w),i.add(f.ticket),c[g?"linked":"created"]++,s.push(`${h} ${g?"linked":"created"} ${f.ticket}`)}catch(k){if(!(k instanceof re))throw k;c.failed++,s.push(`${h} failed: ${k.message}`)}}const u=t?`would link ${c.linked}, create ${c.created}`:`linked ${c.linked}, created ${c.created}, failed ${c.failed}`;return{lines:[...s,`backfill${t?" (dry run)":""}: ${u}`]}}r(dr,"backfill");const jd=r(e=>Ee(e,async()=>{const t=[];for(const n of await F(e)){const a=await $(n.file,null);if(!Array.isArray(a?.items)||!Array.isArray(a.followups))continue;const i=a.followups.find(d=>a.items.some(u=>u.id===d.id));i&&l(`${n.slug}: follow-up ${i.id} has the id of a card; rename one, then restart the board`);const o=[];for(const{id:d,title:u,from:p,filed:w,done:f,...h}of a.followups){const g=f?e&&(await G(M(e,`${n.slug}/${d}`))).map(k=>k.ts).filter(Boolean).sort().pop()||w:null;o.push({id:d,title:u,...w?{created:w}:{},...p?{parent:J(n.slug,p)}:{},...f?{lane:"done",...g?{lane_at:g,closed_at:g}:{}}:{},...h})}const{followups:s,...c}=a;await y(n.file,{...c,items:[...a.items,...o]}),t.push(`${n.slug}: ${o.length} follow-ups moved to cards (${o.filter(d=>d.lane==="done").length} done)`)}return t}),"migrateFollowups");function Di(e,t=[]){if(e.status===void 0&&e.status_at===void 0&&e.merged_at===void 0)return!1;const n=e.merged_at||e.status_at||new Date().toISOString();["merged","done","dropped"].includes(e.status)&&Object.assign(e,{lane:"done",lane_at:n,closed_at:n}),(e.status==="merged"||e.merged_at)&&(e.prs=Oe(e).map(i=>({...i,merged_at:i.merged_at||n})),delete e.pr),e.status==="held"&&(e.held=!0);const a=t.filter(i=>i.started&&Je(i)).sort((i,o)=>String(i.started).localeCompare(String(o.started))).at(-1);return e.status==="checking"&&a&&(a.handed_back=n),delete e.status,delete e.status_at,delete e.merged_at,!0}r(Di,"statusToFacts");const Od=r(e=>Ee(e,async()=>{const t=[];for(const n of await F(e)){const a=await $(n.file,null);let i=0;for(const o of Array.isArray(a?.items)?a.items:[]){const s=o.status==="checking"?await H(n.dir,o.id):[],c=await Promise.all(s.map(d=>$(d,null)));if(Di(o,c.filter(Boolean))){i++;for(const[d,u]of c.entries())u?.handed_back&&await y(s[d],u)}}i&&(await y(n.file,a),t.push(`${n.slug}: ${i} cards' statuses became lanes and facts`))}return await xt(e,async()=>{const n=await Me(e),a=n.filter(i=>i.item&&Di(i.item)).length;a&&(await vt(e,n),t.push(`archive: ${a} cards' statuses became lanes and facts`))}),t}),"migrateStatuses");function Ai(e,t){const n=e.lane==="done"&&!un.done.includes(e.phase);if(!("state"in e)&&!n)return!1;if("state"in e&&e.state!==void 0&&(e.phase=e.state),delete e.state,n){const a=Oe(e);e.phase=a.some(i=>i.merged_at)||!a.length&&t?"merged":"closed"}return!0}r(Ai,"stateToPhase");const Pd=r(e=>Ee(e,async()=>{const t=[],n=await F(e),a=r(async(i,o)=>!!i&&o.lane==="done"&&(await Promise.all((await H(i,o.id)).map(s=>$(s,null)))).some(s=>s?.started&&Je(s)),"built");for(const i of n){const o=await $(i.file,null);let s=0;for(const c of Array.isArray(o?.items)?o.items:[])Ai(c,await a(i.dir,c))&&s++;s&&(await y(i.file,o),t.push(`${i.slug}: ${s} cards' states became phases`))}return await xt(e,async()=>{const i=await Me(e);let o=0;for(const s of i)s.item&&Ai(s.item,await a(n.find(c=>c.slug===(s.wave||I(s.key).wave))?.dir,s.item))&&o++;o&&(await vt(e,i),t.push(`archive: ${o} cards' states became phases`))}),t}),"migratePhases"),Dd=r(e=>Ee(e,async()=>{const t=[];for(const n of await F(e)){const a=await $(n.file,null),i=(Array.isArray(a?.items)?a.items:[]).filter(o=>o.implement_at);if(i.length){for(const o of i)o.phase!=="ready"&&Object.assign(o,{phase:"staging",staged_at:o.implement_at}),delete o.implement_at;await y(n.file,a),t.push(`${n.slug}: ${i.length} Implement asks moved to staging or ready`)}}return t}),"stageImplementAsks"),Xt=r(e=>m.join(e,"data","done-archive.json"),"archiveFile"),xt=r((e,t)=>wt(m.join(e,"done-archive"),t,{heartbeat:!0}),"archiveWrite"),Me=r(async e=>(await $(Xt(e),{cards:[]})).cards||[],"readArchive"),en=r(e=>`${e.closed_at||""}|${e.key}`,"archiveRank"),lr=r((e,t)=>en(e)<en(t)?1:en(e)>en(t)?-1:0,"byClosed"),Jn=r(e=>m.join(e,"data","done-archive-count.json"),"archiveCountFile");async function Ad(e){const t=(await $(Jn(e),null))?.count;if(Number.isInteger(t))return t;const n=(await Me(e)).length;return await y(Jn(e),{count:n}),n}r(Ad,"archiveCount");async function vt(e,t){t.length?await y(Xt(e),{cards:t.sort(lr)}):await P.rm(Xt(e),{force:!0}),await y(Jn(e),{count:t.length})}r(vt,"writeArchive");const Rd=r((e,t)=>xt(e,async()=>{const n=new Map(t.map(w=>[w.key,w])),a=new Date().toISOString(),i=[],o=[];for(const w of await F(e)){const f=await $(w.file,null);if(!Array.isArray(f?.items))continue;const h=f.items.filter(g=>n.has(`${w.slug}/${g.id}`)&&g.lane==="done");if(h.length){for(const g of h){const k=n.get(`${w.slug}/${g.id}`);i.push({...k,wave:w.slug,archived_at:a,item:k.prs?ur(g,k.prs):g})}o.push([w.file,{...f,items:f.items.filter(g=>!h.includes(g))}])}}if(!i.length)return[];const s=await $(L(e),{items:[]}),c=new Map(i.map(w=>[`item:${w.key}`,w]));for(const w of i)w.questions=(s.items||[]).filter(f=>f.topic===`item:${w.key}`&&f.resolved);const d=await De(e);for(const w of i)w.asks=w.item.from_ask&&X(d,w.item.from_ask)===void 0?[w.item.from_ask]:[];const u=new Set(i.map(w=>w.key));await vt(e,[...(await Me(e)).filter(w=>!u.has(w.key)),...i.map(({key:w,wave:f,archived_at:h,closed_at:g,item:k,questions:v,asks:D})=>({key:w,wave:f,archived_at:h,closed_at:g,item:k,questions:v,asks:D}))]),i.some(w=>w.asks.length)&&await y(ke(e),{links:{...d,...Object.fromEntries(i.flatMap(w=>w.asks.map(f=>[f,w.key])))}});for(const[w,f]of o)await y(w,f);const p=(s.items||[]).filter(w=>!(c.has(w.topic)&&w.resolved));return p.length!==(s.items||[]).length&&await y(L(e),{...s,items:p}),[...u]}),"archiveCards");function ur(e,t){if(!Ce(e))return e;const n=Oe(e).map(o=>{const s=t.find(c=>c.state&&c.number===o.number&&(!o.repo||o.repo===c.repo));return s?{...o,kept:{checks:s.checks,reviews:s.reviews,codexOpen:s.codexOpen}}:o}),{pr:a,...i}=e;return{...i,prs:n}}r(ur,"keptPRs");async function fr(e,t){return!e||!fe(Xt(e))?!1:xt(e,async()=>{const n=await Me(e),a=n.find(w=>w.key===t);if(!a)return!1;const i=(await F(e)).find(w=>w.slug===a.wave),o=i&&await $(i.file,null);if(!Array.isArray(o?.items))return!1;o.items.some(w=>w.id===a.item.id)||await y(i.file,{...o,items:[...o.items,a.item]});const s=await $(L(e),{items:[]}),c=new Set((s.items||[]).map(w=>w.id)),d=(a.questions||[]).filter(w=>!c.has(w.id));d.length&&await y(L(e),{...s,items:[...s.items||[],...d]});const u=await De(e),p=(a.asks||[]).filter(w=>X(u,w)===t);return p.length&&await y(ke(e),{links:Object.fromEntries(Object.entries(u).filter(([w])=>!p.includes(w)))}),await vt(e,n.filter(w=>w!==a)),!0})}r(fr,"restoreArchived");const Ri=["push_tag","no_change"],Ni=r(e=>Object.fromEntries(Object.entries(e).filter(([t])=>!Ri.includes(t))),"stripQuestion"),Ei=r(({summary:e,...t})=>t,"stripItem"),Ii=r(e=>Ri.some(t=>t in e),"holdsDead"),Nd=r(e=>xt(e,async()=>{for(const a of await F(e)){const i=await $(a.file,null);Array.isArray(i?.items)&&i.items.some(o=>"summary"in o)&&await y(a.file,{...i,items:i.items.map(Ei)})}const t=await $(L(e),null);(t?.items||[]).some(Ii)&&await y(L(e),{...t,items:t.items.map(Ni)});const n=await Me(e);n.some(a=>"card"in a||"summary"in a.item||(a.questions||[]).some(Ii))&&await vt(e,n.map(({card:a,...i})=>({...i,...a?{closed_at:i.closed_at||a.sortAt}:{},item:Ei(i.item),questions:(i.questions||[]).map(Ni)})))}),"stripDeadFields"),Bi=r(e=>`lineup-${e}.md`,"lineupNoteFile"),wr=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:l(`a wave's slug uses only a-z, 0-9 and "-", not "${e}"`),"lineupSlug");async function tn(e,t){for(const n of await F(e)){const a=await $(n.file,null),i=(a?.lineups||[]).find(o=>o.slug===t);if(i)return{source:n,wave:a,lineup:i}}return l(`this board has no wave "${t}"; lineup create makes one`)}r(tn,"findLineup");async function pr(e,t){(await ie(e)).items!=="waves"&&l(`topic wave:${t} is for a board over ~/.claude/waves; a lineup's triage uses lineup:<slug>`),await Te(e,t)}r(pr,"triagedWave");function _t(e,t,n,a){return n.map(i=>{const{wave:o,id:s}=I(i);o!==e.slug&&l(`${i} is not in wave file "${e.slug}"; a wave's cards share one file`),t.items.some(d=>d.id===s)||l(`wave "${o}" has no item "${s}"`);const c=(t.lineups||[]).find(d=>d.slug!==a&&!d.finished_at&&wn(d).flat().includes(s));return c&&l(`${i} is already in the wave "${c.slug}"; lineup remove ${c.slug} ${i} first`),s})}r(_t,"lineupCards");function Qn(e,t){t!==void 0&&l("--phase is now --step: pass one --step per wave step, as <wave>/<id>,<wave>/<id>, in build order");const n=[e??[]].flat().map(a=>String(a).split(",").map(i=>i.trim()).filter(Boolean));return(!n.length||n.some(a=>!a.length))&&l("--step takes one step's cards, as <wave>/<id>,<wave>/<id>, once per step in build order"),n}r(Qn,"stepKeys");function Li(e,t,n,a){const i=new Set(n.map(o=>`${e.slug}/${o}`));for(const o of n){const s=t.items.find(u=>u.id===o),c=(s.deps||[]).filter(u=>!i.has(J(e.slug,u))),d=a.findIndex(u=>u.includes(o));d>0&&c.push(...a[d-1]),c.length?s.deps=c:delete s.deps}}r(Li,"chain");async function Ci(e,t,n,a,i){i!==void 0&&await mt(m.join(t.dir,Bi(a.slug)),`${S(i,"the note").replace(/\n*$/,`
|
|
29
|
+
`)}`),await y(t.file,n);const{tickets:o}=be(await ie(e));return{file:t.file,lines:[`order: ${Ti(n,a.phases,o)}`]}}r(Ci,"writeLineup");const Ti=r((e,t,n)=>t.map(a=>a.map(i=>Qe(e,e.items.find(o=>o.id===i)||{id:i},n)).join(", ")).join(" \u2192 "),"orderLine");async function Ed(e,t,{title:n,goal:a,steps:i,phase:o,note:s}){wr(t);const c=Qn(i,o);s===void 0&&l("lineup create needs the wave note, as --note <text> or --file <path>");const d=await F(e);d.some(h=>h.slug===t)&&l(`"${t}" is a wave file on this board; pick another slug`);for(const h of d)((await $(h.file,null))?.lineups||[]).some(g=>g.slug===t)&&l(`this board already has a wave "${t}"`);const u=d.find(h=>h.slug===I(c[0][0]).wave)||l(`this board has no wave "${I(c[0][0]).wave}"`),{wave:p}=await Te(e,u.slug),w=c.map(h=>_t(u,p,h,t));new Set(w.flat()).size!==w.flat().length&&l("--step names a card twice");const f={slug:t,title:S(n,"--title"),goal:S(a,"--goal"),phases:w,created:new Date().toISOString()};return Li(u,p,w.flat(),w),p.lineups=[...p.lineups||[],f],Ci(e,u,p,f,s)}r(Ed,"createLineup");async function Id(e,t,{add:n,after:a,remove:i,steps:o,phase:s,title:c,goal:d,note:u}){s!==void 0&&Qn(void 0,s),[n,i,o,c,d,u].every(g=>g===void 0)&&l("nothing to change: pass --add, --remove, --step, --title, --goal, --note or --file"),a!==void 0&&n===void 0&&l("--after goes with --add");const{source:p,wave:w,lineup:f}=await tn(e,t);f.finished_at&&l(`the wave "${t}" is finished`),f.phases=wn(f),delete f.items;const h=f.phases.flat();if(n!==void 0){const[g]=_t(p,w,[n],t);h.includes(g)&&l(`${n} is already in the wave "${t}"`);const k=a===void 0?f.phases.length:f.phases.findIndex(v=>v.includes(_t(p,w,[a],t)[0]))+1;k===0&&l(`--after ${a} is not in the wave "${t}"`),f.phases.splice(k,0,[g])}if(i!==void 0){const[g]=_t(p,w,[i],t);h.includes(g)||l(`${i} is not in the wave "${t}"`),h.length===1&&l(`${i} is the wave's last card; lineup finish ${t} ends the wave`),f.phases=f.phases.map(k=>k.filter(v=>v!==g)).filter(k=>k.length)}if(o!==void 0){const g=Qn(o).map(v=>_t(p,w,v,t)),k=g.flat();(k.length!==h.length||new Set(k).size!==k.length||k.some(v=>!h.includes(v)))&&l("--step lists each of the wave's cards once"),f.phases=g}return c!==void 0&&(f.title=S(c,"--title")),d!==void 0&&(f.goal=S(d,"--goal")),Li(p,w,[...new Set([...h,...f.phases.flat()])],f.phases),Ci(e,p,w,f,u)}r(Id,"changeLineup");async function Bd(e,t){const{source:n,wave:a,lineup:i}=await tn(e,t);return i.finished_at=new Date().toISOString(),await y(n.file,a),{file:n.file}}r(Bd,"finishLineup");async function Ld(e,t,n){const{source:a,wave:i,lineup:o}=await tn(e,t),s=await P.readFile(m.join(a.dir,Bi(t)),"utf8").catch(()=>"");return{title:o.title,markdown:`**Goal** ${o.goal}
|
|
28
30
|
|
|
29
|
-
**Order** ${
|
|
31
|
+
**Order** ${Ti(i,wn(o),n)}
|
|
30
32
|
|
|
31
|
-
${s}`}}r(nl,"readLineupNotes");async function C(e,t,n,a="main"){const{id:i}=N(t);/^[\w-]{1,40}$/.test(a)||d(`--agent takes a name of letters, digits, "_" and "-", not "${a}"`);const o=a==="main"?`${i}.json`:`${i}@${a}.json`;if(n)return $.join($.resolve(n),"agents",o);const{source:s}=await v(e,t);return $.join(s.dir,"agents",o)}r(C,"builderFile");const wi=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"),Ts=["reading","building","testing","checking","landing"],mi=80,hi="reading, building, testing, checking or landing";function gi(e,t,n,a){e||d(`${a} needs --site, since the line goes to the item's thread`),t!==void 0&&!Ts.includes(t)&&d(`--phase takes ${hi}, not "${t}"`);const i=j(n,a);return i.length>mi&&d(`${a} is ${i.length} characters; the cap is ${mi}. Say it shorter.`),{phase:t,detail:i}}r(gi,"progressLine");const $i=r((e,t,n)=>t?{detail:e.detail}:(e.phase===void 0&&d(`${n} needs --phase ${hi}`),e),"forAgent");async function Bn(e,t,n="main",a,{phase:i,detail:o},s){const c=Ce(a),l=c||n==="main"?"":a.kind===ae?n:_t(a)||a.kind||n,u=c?Ci[a.kind]:[l,i].filter(Boolean).join(" "),w=new RegExp(`^(?:${u}|${i??u})\\s*:?\\s+`,"i"),p=u?`${u[0].toUpperCase()}${u.slice(1)}: ${o.replace(w,"")}`:o,f=U(e,t);(await K(f)).filter(m=>m.kind==="status").pop()?.text!==p&&await V(f,{ts:s,who:"claude",kind:"status",run:a.started,...i?{phase:i}:{},...c?{planner:!0}:{},text:p})}r(Bn,"statusToThread");const qs={prototype:"sonnet",grill:"opus",[le]:"sonnet"};async function ft(e,t,{label:n,phase:a,progress:i,dir:o,agent:s="main",kind:c,model:l,eta:u,worktree:w,ownWorktree:p,dispatch:f}){a!==void 0&&i===void 0&&d('--phase goes with --progress "<detail>"');const m=Y(i,W=>gi(e,a,W,"--progress")),h=await C(e,t,o,s),k=new Date().toISOString(),x=Y(c,W=>/^[\w.-]{1,30}$/.test(W)?W:d("--kind takes a short word such as claude or codex")),E=et(x),O=l??(x==="opus"||x==="sonnet"?x:void 0),B=["build",...Js].includes(E),_=await g(h,null),S=r(W=>["tap","hub"].includes(W?.dispatched)&&!W.done,"tapOpen");if(f==="session"&&S(_))return{file:h,alreadyOpen:!0,lines:[]};s!=="main"&&x!==le&&x!==ae&&S(await g(await C(e,t,o),null))&&d(`the Implement tap opened ${t}'s main builder record and no agent has taken it up; run builder start ${t} without --agent first`);const D=_?.dispatched&&!_.done?_:null,F=B&&!o?(await v(e,t)).item:void 0,M=_&&Ee(_).length?_:null,q=D&&E==="build"?{kind:et(D.kind),model:D.model,reason:{tap:"The owner's Build tap picked it.",hub:"The board's pull picked it."}[D.dispatched]??"The session's dispatch picked it."}:B?sn({card:F??{}}):null;q&&E!=="build"&&E!==q.kind&&d(`--kind ${E}: ${q.kind} builds now. ${q.reason} Pass --kind ${q.kind}, or --kind build.`);const I=q?q.kind:E,oe=Xn({dir:e});if(Ui(oe,I))throw new ie(Wi(oe,I));const pe=m&&$i(m,Ce({kind:I}),"--progress");l!==void 0&&!["opus","sonnet"].includes(l)&&d("--model takes opus or sonnet"),O!==void 0&&I==="codex"&&d("--model is for kind claude; codex needs none");const se=I==="claude"?rn():"auto";O!==void 0&&se!=="auto"&&O!==se&&d(`--model ${O}: the hub's Claude model is ${se==="opus"?"Opus":"Sonnet"}. Pass --model ${se}, or no --model.`);const $e=I==="claude"?O??q?.model??(D&&et(D.kind)==="claude"?D.model:void 0)??"opus":Ce({kind:I})?O??(D?.kind===I?D.model:void 0)??qs[I]:I===ae?O??_?.model:void 0,ke={label:n===void 0?"Builder":j(n,"--label"),...I?{kind:I}:{},...$e?{model:$e}:{},started:k,...u===void 0?{}:{eta:wi(u,"--eta")},...f?{dispatched:f}:{},...M?{calls:M.calls,...M.verified?{verified:M.verified}:{}}:{},...Hs(_)?{session:_.session}:{},...I===ae&&_?.kind===ae?{label:_.label,task:_.task,writes:_.writes,requested:_.requested}:{}},Le=B&&!o&&!G(F)&&F.lane!=="progress",wt=Le?await fe(e,t,{start:!0}):{lines:[]};let ce,be=null;const ye=p&&B&&s==="main"?[_?.worktree,F?.worktree].find(W=>W?.path&&me(W.path))??null:null,mt=(D??M)?.worktree??ye,ht=w??(B&&s==="main"&&!mt?p:void 0);try{if(ht){const W=await z($.dirname($.dirname(h)),N(t).id),Gt=(await Promise.all(W.filter(ee=>ee!==h).map(ee=>g(ee,null)))).filter(ee=>ee?.worktree?.path&&!ee.done).map(ee=>ee.worktree.path);({remade:be,...ce}=await ht({busy:Gt}))}else ce=mt}catch(W){throw Le&&await Ks(e,t,F),W}await y(h,ce?{...ke,worktree:ce}:ke),F?.rebase_run&&s==="main"&&![ki,"Lander"].includes(ke.label)&&await bi(e,t),q?.inputs&&e&&io(ea(e),{call:"builder-pick",key:t,result:[I,I==="claude"?$e:null].filter(Boolean).join(" "),reason:q.reason,inputs:{...q.inputs,...O?{askedModel:O}:{},...f?{dispatch:f}:{}}}),be&&e&&await J(e,t,{text:be}),pe&&await Bn(e,t,s,ke,pe,k);const Jt=x===le||x===ae?[]:await An(e,t,"Acted on: builder started");return e&&B&&await ps(e,t),{file:h,kind:I??null,model:$e??null,reason:q?.reason??null,worktree:ce?.path??null,lines:[...E==="build"?[`kind: ${I}. ${q.reason}`]:[],..._t(ke)?[`model: ${_t(ke)}`]:[],...e?[`ref: ${await X(e,t)}`]:[],...ce?[`worktree: ${ce.path}`]:[],...be?[be]:[],...wt.lines,...Jt]}}r(ft,"startBuilder");async function Fs(e,t,n,a="tap"){try{const{held:i,blockers:o}=await Ws(e,t);if(i&&await Va(e,"card-hold",{key:t,hold:!1}),o.length)return await Us(e,t,o);try{const s=await ft(e,t,{kind:"build",worktree:n,dispatch:a});return{key:t,ref:await X(e,t),kind:s.kind,...s.model?{model:s.model}:{},reason:s.reason,worktree:s.worktree}}catch(s){throw i&&await zs(e,t,i),s}}catch(i){if(!(i instanceof ie))throw i;return await V(U(e,t),{ts:new Date().toISOString(),who:"claude",kind:Vn,text:`Couldn't start implementing: ${i.message}`}),null}}r(Fs,"startTappedBuild");async function al(e,t,{pr:n,conflict:a,base:i},o){const{source:s,wave:c,item:l}=await v(e,t);l.rebase_run={pr:n,base:i,conflict:a,at:new Date().toISOString()},await y(s.file,c);try{const u=await ft(e,t,{kind:"build",label:ki,dispatch:"hub",ownWorktree:o});return{key:t,kind:u.kind,...u.model?{model:u.model}:{},worktree:u.worktree}}catch(u){throw await bi(e,t),u}}r(al,"startRebase");const ki="Rebase";async function bi(e,t){const{source:n,wave:a,item:i}=await v(e,t);delete i.rebase_run,await y(n.file,a)}r(bi,"endRebaseRun");async function il(e,t){const{wave:n,item:a}=await v(e,t),i=ne(e,n);return Ae(a,i).find(o=>o.repo===i)?.number??null}r(il,"cardPR");async function ol(e,t,{from:n,to:a,replayed:i}){const{source:o,wave:s,item:c}=await v(e,t);return c.rebase_run?(c.rebased=[...c.rebased||[],{from:n,to:a,base:c.rebase_run.base,replayed:i,at:new Date().toISOString()}],delete c.rebase_run,await y(o.file,s),!0):!1}r(ol,"recordRebase");const Ms=r((e,t,n)=>{const a=new Map((e.rebased||[]).filter(i=>i.replayed).map(i=>[i.to,i.from]));for(let i=n,o=0;i&&o<=a.size;i=a.get(i),o++)if(i===t)return!0;return!1},"onlyRebased");async function Ws(e,t){const n=Xn({dir:e});Hi(n)&&d(Mi(n));const{item:a}=await v(e,t),i=a.held?{held:a.held,pick_up_at:a.pick_up_at,held_at:a.held_at}:null;return i&&Je(a)&&d("it's held. Unhold it, then tap Implement."),await Dn(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 ys(e,t,a)}}r(Ws,"tapRefusals");async function sl(e,t,n,a){await J(e,t,{text:Oe.implement,who:"love",by:a,command:"implement"});const{item:i}=await v(e,t),o={state:i.state,unfiled:i.unfiled};await yi(e,t,{state:"ready",unfiled:void 0});const s=await Fs(e,t,n);return s||await yi(e,t,o),s}r(sl,"implementAsIs");async function yi(e,t,{state:n,unfiled:a}){const{source:i,wave:o,item:s}=await v(e,t);n===void 0?delete s.state:s.state=n,a===void 0?delete s.unfiled:s.unfiled=a,await y(i.file,o)}r(yi,"writeCardState");async function Us(e,t,n){const a=n.join(", ");return await re({dir:e},"card-bump",{key:t}),await V(U(e,t),{ts:new Date().toISOString(),who:"claude",text:`Can\u2019t start yet: waits on ${a}. Bumped to the top of Todo.`}),{key:t,ref:await X(e,t),bumped:n,line:`bumped: ${t} waits on ${a}, so it stays in Todo, bumped to the top`}}r(Us,"bumpBlocked");async function rl(e,t,n){const a=await C(e,t),i=await g(a,null),o=new Date().toISOString();i&&!i.done&&await y(a,{...i,done:o});const{source:s,wave:c,item:l}=await v(e,t);l.lane==="progress"&&Rn(l,"todo",o),await y(s.file,c),await V(U(e,t),{ts:o,who:"claude",kind:Vn,text:`Couldn't start implementing: ${n}`})}r(rl,"abandonLaunch");async function cl(e,t,n,a="main"){const i=await C(e,t,void 0,a),o=await g(i,null);o&&!o.done&&await y(i,{...o,done:new Date().toISOString()}),await Bt(e,t),await J(e,t,{text:n})}r(cl,"abandonPlanner");async function ll(e,t,n,a,i="main",o=null){const s=await C(e,t,void 0,i),c=await g(s,null);await y(s,{...c,session:{id:n,pid:a,log:o,at:new Date().toISOString()}})}r(ll,"recordSession");async function dl(e,t){const{source:n}=await v(e,t);return(await Promise.all((await z(n.dir,N(t).id)).map(i=>g(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({pid:i.session?.pid??null,at:i.session?.at??null}))}r(dl,"openAgents");async function ul(e,t){const{source:n}=await v(e,t);return(await Promise.all((await z(n.dir,N(t).id)).map(i=>g(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({kind:i.kind??null,pid:i.session?.pid??null,at:i.session?.at??null}))}r(ul,"openRuns");async function fl(e,t,n){const{source:a}=await v(e,t),i=new Date().toISOString();for(const o of await z(a.dir,N(t).id)){const s=await g(o,null);s?.started&&!s.done&&Xe(s)&&await y(o,{...s,stopped:{run:n,at:i}})}}r(fl,"markStopped");async function pl(e,t){const{source:n,wave:a,item:i}=await v(e,t);i.merge_ready&&(i.merge_ready.stopped=new Date().toISOString(),await y(n.file,a))}r(pl,"stopHeldMerge");async function wl(e,t){const{source:n}=await v(e,t),a=await K($.join(e,"data","events.jsonl")),i=r(l=>a.filter(u=>u.kind===l&&u.data?.key===t).map(u=>String(u.ts)).sort().pop()||"","latest"),o=i("card-bump");if(!o||i("card-unbump")>o)return null;const c=(await Promise.all((await z(n.dir,N(t).id)).map(l=>g(l,null)))).filter(l=>l&&Xe(l)).map(l=>String(l.started||"")).filter(Boolean).sort()[0]||"";return c&&o<c?null:o}r(wl,"landFirst");async function ml(e,t,n){const{item:a}=await v(e,t);G(a)&&d(`${await X(e,t)} is done; there is nothing to bump`);const i=await re({dir:e},n?"card-bump":"card-unbump",{key:t});return(await K($.join(e,"data","events.jsonl"))).find(o=>o.id===i).ts}r(ml,"bumpCard");async function hl(e,t){return(await K(U(e,t))).filter(n=>n.kind==="status").pop()?.phase==="landing"}r(hl,"isLanding");const Hs=r(e=>!!e?.session&&e.session.at>(e.done??e.started),"launchedSession");async function gl(e,t,n){const a=await g(await C(e,t),null);if(a&&!a.done)return null;const i=await ft(e,t,{kind:n,dispatch:"hub"});return{key:t,kind:n,model:i.model}}r(gl,"startPlanner");async function $l(e,t,n){const{source:a}=await v(e,t);if((await Promise.all((await z(a.dir,N(t).id)).map(c=>g(c,null)))).some(c=>c?.kind===le&&c.started&&!c.done))return null;const o=Fi(n);if(await g(await C(e,t,void 0,o),null))return null;const s=await ft(e,t,{kind:le,agent:o,dispatch:"hub"});return{key:t,kind:le,model:s.model,agent:o}}r($l,"startVisual");const Cn=r(e=>typeof e=="string"&&/^[a-z][a-z0-9-]{0,29}$/.test(e)&&e!=="main"&&!e.startsWith(`${le}-`)?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 Tn(e,t){const{item:n}=await v(e,t);return[(await g(await C(e,t),null))?.worktree,n.worktree].find(i=>i?.path&&me(i.path))??null}r(Tn,"cardWorktree");async function kl(e,t,{name:n,task:a,writes:i,model:o}){P(e);const s=Cn(n),c=j(a,"--task");o!==void 0&&!["opus","sonnet"].includes(o)&&d("--model takes opus or sonnet");const l=rn();o!==void 0&&l!=="auto"&&o!==l&&d(`--model ${o}: the hub's Claude model is ${l==="opus"?"Opus":"Sonnet"}. Pass --model ${l}, or no --model.`);const{item:u}=await v(e,t);G(u)&&d(`${await X(e,t)} is Done; a helper needs an open card`);const w=await C(e,t,void 0,s);return me(w)&&d(`${t} already has an agent named ${s}; pick another --name`),i&&!await Tn(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(w,{label:s,kind:ae,task:c,writes:!!i,...o?{model:o}:{},requested:new Date().toISOString()}),await J(e,t,{text:`Asked for helper ${s}${i?", which writes code":""}: ${c}`}),{lines:[`helper ${s} requested: the board launches it ${i?"once an implementer slot is free":"now"}`,`wait for it with agent wait ${t} --name ${s}`]}}r(kl,"requestHelper");async function bl(e,t,n,a){const i=await C(e,t,void 0,n),o=await g(i,null);if(o?.kind!==ae||o.started||o.done)return null;const{item:s}=await v(e,t),c=sn({card:s}),l=rn(),u=o.model??(l!=="auto"?l:c.kind==="claude"?c.model:"opus"),w=o.writes?await Tn(e,t):null;o.writes&&!w&&d(`${t} has no worktree for helper ${n} to branch from`),await y(i,{...o,model:u});const p=await ft(e,t,{agent:n,kind:ae,dispatch:"hub",worktree:w?({busy:f})=>a({repo:w.repo,base:w.branch,busy:f}):void 0});return{key:t,kind:ae,model:u,agent:n,task:o.task,writes:o.writes,worktree:p.worktree}}r(bl,"startHelper");async function yl(e,t,n){const a=await g(await C(e,t,void 0,Cn(n)),null);return a?.kind!==ae&&d(`${t} has no helper named ${n}`),{record:a,card:await Tn(e,t)}}r(yl,"helperOf");async function xl(e,t,{name:n,agent:a="main",timeout:i=9,every:o=2e3,alive:s}){P(e);const c=Number(i);c>0||d("--timeout takes minutes, a number above 0");const l=await C(e,t,void 0,Cn(n)),u=await C(e,t,void 0,a),w=r(async f=>{const m=await g(u,null);if(!m?.started||m.done)return;const{waits:h,...k}=m;await y(u,f?{...k,waits:f}:k)},"mark"),p=Date.now()+c*6e4;await w(n);try{for(;;){const f=await g(l,null);if(f?.kind!==ae&&d(`${t} has no helper named ${n}`),f.done){const h=f.writes&&f.worktree?[`branch ${f.worktree.branch} in ${f.worktree.path}: merge it with agent merge ${t} --name ${n}`]:[];return{lines:[`helper ${n} done: ${f.note??"no hand-back"}`,...h]}}const m=f.session?.pid??null;m&&!s(m)&&d(`helper ${n}'s process exited without builder done; do its task yourself`),Date.now()>=p&&d(`helper ${n} is still ${f.started?"running":f.writes?"waiting for an implementer slot":"waiting to launch"} after ${c} minutes; run agent wait again`),await new Promise(h=>setTimeout(h,o))}}finally{await w(null)}}r(xl,"waitHelper");async function vl(e,t,n="main"){return n==="main"?!1:(await g(await C(e,t,void 0,n),null))?.kind===ae}r(vl,"isHelper");async function _l(e,t,{file:n,title:a}){P(e);const i=j(a,"--title");(typeof n!="string"||!n.endsWith(".html"))&&d("--file takes the page, an .html file");const o=await A.readFile($.resolve(n),"utf8").catch(()=>d(`--file ${n} can't be read`));o.trim()||d(`--file ${n} is empty`),await v(e,t);const{id:s}=N(t),c=$.join(e,"public","visuals",s);await A.mkdir(c,{recursive:!0});const l=i.toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,60).replace(/^-+|-+$/g,"")||"visual";let u=`${l}.html`;for(let p=2;me($.join(c,u));p++)u=`${l}-${p}.html`;await A.writeFile($.join(c,u),o,{flag:"wx"});const w=`visuals/${s}/${u}`;return await J(e,t,{text:i,link:w,kind:le}),{lines:[w]}}r(_l,"addVisual");async function Sl(e){const t=[];for(const n of await T(e)){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||Ce(o)||t.push({key:`${n.slug}/${i.id}`,kind:et(o.kind),model:o.model})}}return t}r(Sl,"cutOffBuilds");async function zs(e,t,n){const{source:a,wave:i,item:o}=await v(e,t);for(const s of Object.keys(n))n[s]===void 0?delete o[s]:o[s]=n[s];await y(a.file,i)}r(zs,"restoreHold");async function Ks(e,t,n){const{source:a,wave:i,item:o}=await v(e,t);for(const s of["lane","lane_at","closed_at","reopened_at"])n[s]===void 0?delete o[s]:o[s]=n[s];await y(a.file,i)}r(Ks,"restoreLane");async function jl(e,t){return e||d("audit reads a board: pass --site <route>"),t!==void 0&&N(t),{lines:oo(so(ea(e),{key:t}))}}r(jl,"auditLog");const Js=["codex","claude"];async function Ol(e,t){const{item:n}=await v(e,t),{kind:a,model:i,reason:o}=sn({card:n});return{lines:[`${[a,i].filter(Boolean).join(" ")}: ${o}`]}}r(Ol,"pickBuilder");async function xi(e,t,{dir:n}={}){const{id:a}=N(t),i=$.dirname(await C(e,t,n)),o=await z($.dirname(i),a);return(await Promise.all(o.map(c=>g(c,null)))).filter(c=>c?.worktree?.path).sort((c,l)=>String(l.started).localeCompare(String(c.started)))[0]?.worktree??null}r(xi,"recordedWorktree");async function Pl(e,t,n,{phase:a,dir:i,agent:o="main",eta:s,clearEta:c}){s!==void 0&&c&&d("pass --eta or --clear-eta, not both");const l=gi(e,a,n,"builder progress"),u=await C(e,t,i,o),w=await g(u,null);(!w?.started||w.done)&&d(`builder progress needs an open builder record on ${t}; run builder start first`);const p=$i(l,Ce(w),"builder progress");return s!==void 0&&(w.eta=wi(s,"--eta")),c&&delete w.eta,(s!==void 0||c)&&await y(u,w),await Bn(e,t,o,w,p,new Date().toISOString()),u}r(Pl,"builderProgress");async function Al(e,t){await v(e,t);const n=U(e,t);(await K(n)).filter(a=>a.kind==="status").pop()?.phase!=="landing"&&await V(n,{ts:new Date().toISOString(),who:"claude",kind:"status",phase:"landing",text:"Landing: rebasing onto main and landing"})}r(Al,"landingLine");const Ee=r(e=>(e?.calls||[]).filter(t=>!e.verified||t.at>e.verified.at),"unverifiedCalls");async function Dl(e,t,n,{dir:a,agent:i="main"}){const o=j(n,"builder call");/\n/.test(o)&&d("builder call takes one line; record each call on its own");const s=await C(e,t,a,i),c=await g(s,null);return(!c?.started||c.done)&&d(`builder call needs an open builder record on ${t}; run builder start first`),await y(s,{...c,calls:[...c.calls||[],{at:new Date().toISOString(),text:o}]}),s}r(Dl,"recordCall");async function pt(e,t){const n=await K(U(e,t)),a=new Map(n.filter(i=>i.kind===xt.answer).map(i=>[i.ask,i]));return n.filter(i=>i.kind===xt.ask).map(i=>{const o=a.get(i.ask);return{id:i.ask,agent:i.agent||"main",text:i.text,at:i.ts,answer:o?{text:o.text,at:o.ts}:null,deferred:!!o?.deferred}})}r(pt,"builderAsks");const Gs=r(e=>de().find(t=>he(t.dir,e))??{dir:e},"boardTarget");async function Rl(e,t,n,{agent:a="main"}){P(e);const i=j(n,"builder ask");/\n/.test(i)&&d("builder ask takes one line; ask each question on its own");const o=await g(await C(e,t,void 0,a),null);(!o?.started||o.done)&&d(`builder ask needs an open builder record on ${t}; run builder start first`);const s=`ask${(await pt(e,t)).length+1}`;return await V(U(e,t),{ts:new Date().toISOString(),who:`${_t(o)||o.kind||"Claude"} builder`,kind:xt.ask,ask:s,agent:a,text:i}),await re(Gs(e),"builder-ask",{key:t,ask:s,agent:a,text:i}),{id:s,lines:[]}}r(Rl,"askOrchestrator");async function Nl(e,t,n,a,{deferred:i=!1}={}){P(e);const o=j(a,"--text"),s=(await pt(e,t)).find(l=>l.id===n)||d(`no ask "${n}" on ${t}; builder asks ${t} lists them`);s.answer&&d(`${n} on ${t} is already ${s.deferred?"deferred":"answered"}`);const c=new Date().toISOString();if(await V(U(e,t),{ts:c,who:"claude",kind:xt.answer,ask:n,...i?{deferred:!0}:{},text:o}),i){const l=await C(e,t,void 0,s.agent),u=await g(l,null);u&&await y(l,{...u,calls:[...u.calls||[],{at:c,text:`Question: ${s.text} (deferred to the owner)`}]})}return{lines:[]}}r(Nl,"answerBuilder");async function El(e,t){P(e);const n=await pt(e,t);return n.length?{lines:n.flatMap(a=>[`${a.id}. ${a.text}`,` ${a.answer?`${a.deferred?"deferred":"answered"}: ${a.answer.text}`:"waiting on the orchestrator"}`])}:{lines:["no asks yet"]}}r(El,"listAsks");async function Qs(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:<wave>/<id>`),i=(await pt(e,a)).find(o=>o.id===t.ask)||d(`${n}.ask names no ask "${t.ask}" on ${a}; builder asks ${a} lists them`);i.deferred||d(`${n}.ask names ${i.id}, which the orchestrator ${i.answer?"answered":"has not deferred"}; only a deferred ask goes to the owner`)}r(Qs,"checkAskQuestion");async function Ll(e,t,{dir:n,agent:a="main"}){const i=await g(await C(e,t,n,a),null),o=i?.calls||[];if(!o.length)return{lines:["no judgment calls recorded"]};const{verified:s}=i,c=Ee(i).length?"not verified yet":`verified ${s.at}${s.note?`: ${s.note}`:""}`;return{lines:[...o.map((l,u)=>`${u+1}. ${l.text}`),c]}}r(Ll,"listCalls");async function Il(e,t,{dir:n,agent:a="main",note:i}){const o=await C(e,t,n,a),s=await g(o,null);if(Ee(s).length||d(`${t} has no unverified judgment calls${a==="main"?"":` from agent ${a}`}; builder calls ${t} lists them`),e){const c=new Set((await g(L(e),{items:[]})).items?.filter(u=>u.topic===`item:${t}`&&u.ask).map(u=>u.ask)),l=(await pt(e,t)).find(u=>u.deferred&&u.agent===a&&!c.has(u.id));l&&d(`${l.id} on ${t} was deferred to the owner; add its question with "ask": "${l.id}" first`)}return await y(o,{...s,verified:{at:new Date().toISOString(),...i===void 0?{}:{note:j(i,"--note")}}}),o}r(Il,"verifyCalls");async function Bl(e,t){const{source:n,item:a}=await v(e,t),i=r((s,c)=>[(s.options||[]).find(l=>l.id===c.choice)?.label,c.note].filter(Boolean).join(": ")||"answered","said"),o=(await ue(e,t)).asked.filter(s=>s.ask&&s.answers.length).map(s=>({ask:s.ask,question:s.question,answer:i(s,s.answers.at(-1))}));return{key:t,ref:await X(e,t),title:a.title||t,notes:a.plan?$.join(n.dir,a.plan):null,worktree:(await xi(e,t))?.path??null,answers:o,links:await Zs(e,t),rebasePR:a.rebase_run?.pr??null}}r(Bl,"briefFacts");async function Zs(e,t){const n=new Map;for(const c of await T(e))for(const l of(await g(c.file,null))?.items||[])n.set(`${c.slug}/${l.id}`,{entry:c,it:l});const a=N(t).wave,{it:i}=n.get(t),o=[...n].filter(([,c])=>(c.it.related||[]).some(l=>H(c.entry.slug,l)===t)).map(([c])=>c),s=[...i.parent?[["parent",H(a,i.parent)]]:[],...(i.deps||[]).map(c=>["depends on",H(a,c)]),...[...new Set([...(i.related||[]).map(c=>H(a,c)),...o])].map(c=>["related",c])].filter(([,c])=>n.has(c));return Promise.all(s.map(async([c,l])=>{const{entry:u,it:w}=n.get(l);return{relation:c,ref:await X(e,l),title:w.title||l,lane:vt[w.lane||"todo"],notes:w.plan?$.join(u.dir,w.plan):null}}))}r(Zs,"cardLinks");async function Cl(e){const t=[];for(const n of await T(e)){const a=await g(n.file,null);for(const i of a?.items||[]){if(G(i))continue;const o=(await Promise.all((await z(n.dir,i.id)).map(u=>g(u,null)))).filter(Boolean);if(!en(o))continue;const s=o.map(u=>u.verified?.at).filter(Boolean).sort().at(-1);if(!s||o.some(u=>Ee(u).length))continue;const c=`${n.slug}/${i.id}`;if((await ue(e,c)).asked.some(u=>u.state==="open")||await qt(e,c,i))continue;const l=o.filter(u=>u.worktree?.path).sort((u,w)=>String(w.started).localeCompare(String(u.started)))[0];l&&me(l.worktree.path)&&t.push({key:c,verified:s,kind:et(l.kind),model:l.model})}}return t}r(Cl,"landable");async function Tl(e,t){const{source:n,item:a}=await v(e,t);(await Promise.all((await z(n.dir,a.id)).map(c=>g(c,null)))).some(c=>c?.started&&!c.done)||d(`${t} has no open builder; only a card whose lander stopped has one to relaunch`),await fe(e,t,{handBack:!0,next:"A lander relaunches to land the verified build"});const o=$.join(e,"data","landers.json"),s=await g(o,{});delete s[t],await A.writeFile(o+".tmp",JSON.stringify(s)+`
|
|
32
|
-
`),await
|
|
33
|
+
${s}`}}r(Ld,"readLineupNotes");async function q(e,t,n,a="main"){const{id:i}=I(t);/^[\w-]{1,40}$/.test(a)||l(`--agent takes a name of letters, digits, "_" and "-", not "${a}"`);const o=a==="main"?`${i}.json`:`${i}@${a}.json`;if(n)return m.join(m.resolve(n),"agents",o);const{source:s}=await x(e,t);return m.join(s.dir,"agents",o)}r(q,"builderFile");const qi=r((e,t)=>typeof e=="string"&&/^\d{4}-\d\d-\d\dT/.test(e)&&!Number.isNaN(Date.parse(e))?new Date(e).toISOString():l(`${t} takes an ISO time such as 2026-10-23T16:30:00-07:00`),"isoTime"),hr=un.progress,Fi=80,Mi="reading, building, testing, checking or landing";function Wi(e,t,n,a){e||l(`${a} needs --site, since the line goes to the item's thread`),t!==void 0&&!hr.includes(t)&&l(`--phase takes ${Mi}, not "${t}"`);const i=S(n,a);return i.length>Fi&&l(`${a} is ${i.length} characters; the cap is ${Fi}. Say it shorter.`),{phase:t,detail:i}}r(Wi,"progressLine");const Ui=r((e,t,n)=>t?{detail:e.detail}:(e.phase===void 0&&l(`${n} needs --phase ${Mi}`),e),"forAgent");async function Gn(e,t,n="main",a,{phase:i,detail:o},s){const c=Be(a),d=c||n==="main"?"":a.kind===se?n:Bt(a)||a.kind||n,u=c?co[a.kind]:[d,i].filter(Boolean).join(" "),p=new RegExp(`^(?:${u}|${i??u})\\s*:?\\s+`,"i"),w=u?`${u[0].toUpperCase()}${u.slice(1)}: ${o.replace(p,"")}`:o,f=M(e,t);(await G(f)).filter(h=>h.kind==="status").pop()?.text!==w&&await z(f,{ts:s,who:"claude",kind:"status",run:a.started,...i?{phase:i}:{},...c?{planner:!0}:{},text:w})}r(Gn,"statusToThread");const mr={prototype:"sonnet",grill:"opus",[we]:"sonnet"};async function St(e,t,{label:n,phase:a,progress:i,dir:o,agent:s="main",kind:c,model:d,eta:u,worktree:p,ownWorktree:w,dispatch:f}){a!==void 0&&i===void 0&&l('--phase goes with --progress "<detail>"');const h=ne(i,te=>Wi(e,a,te,"--progress")),g=await q(e,t,o,s),k=new Date().toISOString(),v=ne(c,te=>/^[\w.-]{1,30}$/.test(te)?te:l("--kind takes a short word such as claude or codex")),D=ft(v),O=d??(v==="opus"||v==="sonnet"?v:void 0),B=["build",..._r].includes(D),N=await $(g,null),_=r(te=>["tap","hub"].includes(te?.dispatched)&&!te.done,"tapOpen");if(f==="session"&&_(N))return{file:g,alreadyOpen:!0,lines:[]};s!=="main"&&v!==we&&v!==se&&_(await $(await q(e,t,o),null))&&l(`the Implement tap opened ${t}'s main builder record and no agent has taken it up; run builder start ${t} without --agent first`);const E=N?.dispatched&&!N.done?N:null,C=B&&!o?(await x(e,t)).item:void 0,T=N&&We(N).length?N:null,W=E&&D==="build"?{kind:ft(E.kind),model:E.model,reason:{tap:"The owner's Build tap picked it.",hub:"The board's pull picked it."}[E.dispatched]??"The session's dispatch picked it."}:B?gn({card:C??{}}):null;W&&D!=="build"&&D!==W.kind&&l(`--kind ${D}: ${W.kind} builds now. ${W.reason} Pass --kind ${W.kind}, or --kind build.`);const j=W?W.kind:D,Q=fa({dir:e});if(ho(Q,j))throw new re(po(Q,j));const ae=h&&Ui(h,Be({kind:j}),"--progress");d!==void 0&&!["opus","sonnet"].includes(d)&&l("--model takes opus or sonnet"),O!==void 0&&j==="codex"&&l("--model is for kind claude; codex needs none");const Z=j==="claude"?$n():"auto";O!==void 0&&Z!=="auto"&&O!==Z&&l(`--model ${O}: the hub's Claude model is ${Z==="opus"?"Opus":"Sonnet"}. Pass --model ${Z}, or no --model.`);const ee=j==="claude"?O??W?.model??(E&&ft(E.kind)==="claude"?E.model:void 0)??"opus":Be({kind:j})?O??(E?.kind===j?E.model:void 0)??mr[j]:j===se?O??N?.model:void 0,ye={label:n===void 0?"Builder":S(n,"--label"),...j?{kind:j}:{},...ee?{model:ee}:{},started:k,...u===void 0?{}:{eta:qi(u,"--eta")},...f?{dispatched:f}:{},...T?{calls:T.calls,...T.verified?{verified:T.verified}:{}}:{},...yr(N)?{session:N.session}:{},...j===se&&N?.kind===se?{label:N.label,task:N.task,writes:N.writes,requested:N.requested}:{}},Ot=B&&!o&&!Y(C)&&C.lane!=="progress",rt=Ot?await pe(e,t,{start:!0}):{lines:[]};let le,xe=null;const Pt=w&&B&&s==="main"?[N?.worktree,C?.worktree].find(te=>te?.path&&fe(te.path))??null:null,ce=(E??T)?.worktree??Pt,Dt=p??(B&&s==="main"&&!ce?w:void 0);try{if(Dt){const te=await H(m.dirname(m.dirname(g)),I(t).id),ve=(await Promise.all(te.filter(ue=>ue!==g).map(ue=>$(ue,null)))).filter(ue=>ue?.worktree?.path&&!ue.done).map(ue=>ue.worktree.path);({remade:xe,...le}=await Dt({busy:ve}))}else le=ce}catch(te){throw Ot&&await vr(e,t,C),te}await gr(g,N),await y(g,le?{...ye,worktree:le}:ye),C?.rebase_run&&s==="main"&&![Hi,la].includes(ye.label)&&await zi(e,t),W?.inputs&&e&&Ao(wa(e),{call:"builder-pick",key:t,result:[j,j==="claude"?ee:null].filter(Boolean).join(" "),reason:W.reason,inputs:{...W.inputs,...O?{askedModel:O}:{},...f?{dispatch:f}:{}}}),xe&&e&&await U(e,t,{text:xe}),ae&&await Gn(e,t,s,ye,ae,k);const an=v===we||v===se?[]:await Jt(e,t,"Acted on: builder started");return e&&B&&await Hs(e,t),{file:g,kind:j??null,model:ee??null,reason:W?.reason??null,worktree:le?.path??null,lines:[...D==="build"?[`kind: ${j}. ${W.reason}`]:[],...Bt(ye)?[`model: ${Bt(ye)}`]:[],...e?[`ref: ${await K(e,t)}`]:[],...le?[`worktree: ${le.path}`]:[],...xe?[xe]:[],...rt.lines,...an]}}r(St,"startBuilder");async function gr(e,t){if(!t?.started||!t.done||!e.endsWith(".json")||m.basename(e).includes("@"))return;const{label:n,kind:a,model:i,started:o,done:s,session:c}=t,d=m.join(m.dirname(e),`${m.basename(e,".json")}@past-${Date.parse(o)}.json`);await y(d,{label:n,...a?{kind:a}:{},...i?{model:i}:{},started:o,done:s,...c?.log?{session:{log:c.log}}:{}})}r(gr,"keepPastRun");async function Cd(e,t,n,a="tap"){try{const{held:i,blockers:o}=await kr(e,t);if(i&&await xi(e,"card-hold",{key:t,hold:!1}),o.length)return await br(e,t,o);try{const s=await St(e,t,{kind:"build",worktree:n,dispatch:a});return{key:t,ref:await K(e,t),kind:s.kind,...s.model?{model:s.model}:{},reason:s.reason,worktree:s.worktree}}catch(s){throw i&&await xr(e,t,i),s}}catch(i){if(!(i instanceof re))throw i;return await z(M(e,t),{ts:new Date().toISOString(),who:"claude",kind:da,text:`Couldn't start implementing: ${i.message}`}),null}}r(Cd,"startTappedBuild");async function Td(e,t,{pr:n,conflict:a,base:i},o){const{source:s,wave:c,item:d}=await x(e,t);d.rebase_run={pr:n,base:i,conflict:a,at:new Date().toISOString()},await y(s.file,c);try{const u=await St(e,t,{kind:"build",label:Hi,dispatch:"hub",ownWorktree:o});return{key:t,kind:u.kind,...u.model?{model:u.model}:{},worktree:u.worktree}}catch(u){throw await zi(e,t),u}}r(Td,"startRebase");const Hi="Rebase";async function zi(e,t){const{source:n,wave:a,item:i}=await x(e,t);delete i.rebase_run,await y(n.file,a)}r(zi,"endRebaseRun");async function qd(e,t){const{wave:n,item:a}=await x(e,t),i=oe(e,n);return Le(a,i).find(o=>o.repo===i)?.number??null}r(qd,"cardPR");async function Fd(e,t,{from:n,to:a,replayed:i}){const{source:o,wave:s,item:c}=await x(e,t);return c.rebase_run?(c.rebased=[...c.rebased||[],{from:n,to:a,base:c.rebase_run.base,replayed:i,at:new Date().toISOString()}],delete c.rebase_run,await y(o.file,s),!0):!1}r(Fd,"recordRebase");const $r=r((e,t,n)=>{const a=new Map((e.rebased||[]).filter(i=>i.replayed).map(i=>[i.to,i.from]));for(let i=n,o=0;i&&o<=a.size;i=a.get(i),o++)if(i===t)return!0;return!1},"onlyRebased");async function kr(e,t){const n=fa({dir:e});mo(n)&&l(wo(n));const{item:a}=await x(e,t),i=a.held?{held:a.held,pick_up_at:a.pick_up_at,held_at:a.held_at}:null;return i&&Ne(a)&&l("it's held. Unhold it, then tap Implement."),await Hn(e,t)&&l("no take is picked on its prototypes, so it prototypes again. Pick a take from the next round, then tap Implement."),{held:i,blockers:await Ys(e,t,a)}}r(kr,"tapRefusals");async function Md(e,t,n){await U(e,t,{text:he.implement,who:"love",by:n,command:"implement"}),await Fs(e,t,{})}r(Md,"implementAsIs");async function br(e,t,n){const a=n.join(", ");return await me({dir:e},"card-bump",{key:t}),await z(M(e,t),{ts:new Date().toISOString(),who:"claude",text:`Can\u2019t start yet: waits on ${a}. Bumped to the top of Todo.`}),{key:t,ref:await K(e,t),bumped:n,line:`bumped: ${t} waits on ${a}, so it stays in Todo, bumped to the top`}}r(br,"bumpBlocked");async function Wd(e,t,n){const{source:a,wave:i,item:o}=await x(e,t),s=new Date().toISOString();Qt(o,"todo",s,"ready"),delete o.staged_at,o.bumped_out={by:n,at:s},await y(a.file,i),await nn(a.dir,o.id,!1,{endHandBack:!0});const c=await K(e,n);return await z(M(e,t),{ts:s,who:"claude",text:`Bumped out by ${c}: back to Todo, ready. It starts again once a slot frees.`}),`${await K(e,t)} bumped out by ${c}`}r(Wd,"sendBackBumpedOut");async function Ud(e,t,n){const a=await q(e,t),i=await $(a,null),o=new Date().toISOString();i&&!i.done&&await y(a,{...i,done:o});const{source:s,wave:c,item:d}=await x(e,t);d.lane==="progress"&&Qt(d,"todo",o),await y(s.file,c),await z(M(e,t),{ts:o,who:"claude",kind:da,text:`Couldn't start implementing: ${n}`})}r(Ud,"abandonLaunch");async function Hd(e,t,n,a="main"){const i=await q(e,t,void 0,a),o=await $(i,null);o&&!o.done&&await y(i,{...o,done:new Date().toISOString()}),await nt(e,t),await U(e,t,{text:n})}r(Hd,"abandonPlanner");async function zd(e,t,n,a,i="main",o=null){const s=await q(e,t,void 0,i),c=await $(s,null);await y(s,{...c,session:{id:n,pid:a,log:o,at:new Date().toISOString()}})}r(zd,"recordSession");async function Kd(e,t){const{source:n}=await x(e,t);return(await Promise.all((await H(n.dir,I(t).id)).map(i=>$(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({pid:i.session?.pid??null,at:i.session?.at??null}))}r(Kd,"openAgents");async function Jd(e,t){const{source:n}=await x(e,t);return(await Promise.all((await H(n.dir,I(t).id)).map(i=>$(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({kind:i.kind??null,pid:i.session?.pid??null,at:i.session?.at??null}))}r(Jd,"openRuns");async function Qd(e,t,n){const{source:a}=await x(e,t),i=new Date().toISOString();for(const o of await H(a.dir,I(t).id)){const s=await $(o,null);s?.started&&!s.done&&Je(s)&&await y(o,{...s,stopped:{run:n,at:i}})}}r(Qd,"markStopped");async function Gd(e,t){const{source:n,wave:a,item:i}=await x(e,t);i.merge_ready&&(i.merge_ready.stopped=new Date().toISOString(),await y(n.file,a))}r(Gd,"stopHeldMerge");async function Yd(e,t,n){const a=S(n,"item stall --reason"),{source:i,wave:o,item:s}=await x(e,t);return s.lane==="done"&&l(`${await K(e,t)} is done; there is nothing to stall`),s.stalled={reason:a,at:new Date().toISOString()},await y(i.file,o),a}r(Yd,"markStalled");async function Zd(e,t){const{source:n,wave:a,item:i}=await x(e,t);i.stalled&&(delete i.stalled,await y(n.file,a))}r(Zd,"clearStall");async function Vd(e,t){const{item:n}=await x(e,t),a=await G(m.join(e,"data","events.jsonl")),i=r(c=>a.filter(d=>d.kind===c&&d.data?.key===t).map(d=>String(d.ts)).sort().pop()||"","latest"),o=i("card-bump");if(!o||i("card-unbump")>o)return null;const s=[n.closed_at,...Oe(n).map(c=>c.merged_at)].filter(Boolean).map(String).sort().pop()||"";return o>s?o:null}r(Vd,"landFirst");async function Xd(e,t,n){const{item:a}=await x(e,t);Y(a)&&l(`${await K(e,t)} is done; there is nothing to bump`);const i=await me({dir:e},n?"card-bump":"card-unbump",{key:t});return(await G(m.join(e,"data","events.jsonl"))).find(o=>o.id===i).ts}r(Xd,"bumpCard");async function el(e,t){return(await G(M(e,t))).filter(n=>n.kind==="status").pop()?.phase==="landing"}r(el,"isLanding");const yr=r(e=>!!e?.session&&e.session.at>(e.done??e.started),"launchedSession");async function tl(e,t,n){const a=await $(await q(e,t),null);if(a&&!a.done)return null;const i=await St(e,t,{kind:n,dispatch:"hub"});return{key:t,kind:n,model:i.model}}r(tl,"startPlanner");async function nl(e,t,n){const{source:a}=await x(e,t);if((await Promise.all((await H(a.dir,I(t).id)).map(c=>$(c,null)))).some(c=>c?.kind===we&&c.started&&!c.done))return null;const o=fo(n);if(await $(await q(e,t,void 0,o),null))return null;const s=await St(e,t,{kind:we,agent:o,dispatch:"hub"});return{key:t,kind:we,model:s.model,agent:o}}r(nl,"startVisual");const Yn=r(e=>typeof e=="string"&&/^[a-z][a-z0-9-]{0,29}$/.test(e)&&e!=="main"&&!e.startsWith(`${we}-`)?e:l('--name takes a short lowercase name such as tests or docs-pass: letters, digits and "-", not main or visual-\u2026'),"helperName");async function Zn(e,t){const{item:n}=await x(e,t);return[(await $(await q(e,t),null))?.worktree,n.worktree].find(i=>i?.path&&fe(i.path))??null}r(Zn,"cardWorktree");async function al(e,t,{name:n,task:a,writes:i,model:o}){A(e);const s=Yn(n),c=S(a,"--task");o!==void 0&&!["opus","sonnet"].includes(o)&&l("--model takes opus or sonnet");const d=$n();o!==void 0&&d!=="auto"&&o!==d&&l(`--model ${o}: the hub's Claude model is ${d==="opus"?"Opus":"Sonnet"}. Pass --model ${d}, or no --model.`);const{item:u}=await x(e,t);Y(u)&&l(`${await K(e,t)} is Done; a helper needs an open card`);const p=await q(e,t,void 0,s);return fe(p)&&l(`${t} already has an agent named ${s}; pick another --name`),i&&!await Zn(e,t)&&l(`a helper that writes branches from ${t}'s worktree, and the card has none; start its build first, or drop --writes`),await y(p,{label:s,kind:se,task:c,writes:!!i,...o?{model:o}:{},requested:new Date().toISOString()}),await U(e,t,{text:`Asked for helper ${s}${i?", which writes code":""}: ${c}`}),{lines:[`helper ${s} requested: the board launches it ${i?"once an implementer slot is free":"now"}`,`wait for it with agent wait ${t} --name ${s}`]}}r(al,"requestHelper");async function il(e,t,n,a){const i=await q(e,t,void 0,n),o=await $(i,null);if(o?.kind!==se||o.started||o.done)return null;const{item:s}=await x(e,t),c=gn({card:s}),d=$n(),u=o.model??(d!=="auto"?d:c.kind==="claude"?c.model:"opus"),p=o.writes?await Zn(e,t):null;o.writes&&!p&&l(`${t} has no worktree for helper ${n} to branch from`),await y(i,{...o,model:u});const w=await St(e,t,{agent:n,kind:se,dispatch:"hub",worktree:p?({busy:f})=>a({repo:p.repo,base:p.branch,busy:f}):void 0});return{key:t,kind:se,model:u,agent:n,task:o.task,writes:o.writes,worktree:w.worktree}}r(il,"startHelper");async function ol(e,t,n){const a=await $(await q(e,t,void 0,Yn(n)),null);return a?.kind!==se&&l(`${t} has no helper named ${n}`),{record:a,card:await Zn(e,t)}}r(ol,"helperOf");async function sl(e,t,{name:n,agent:a="main",timeout:i=9,every:o=2e3,alive:s}){A(e);const c=Number(i);c>0||l("--timeout takes minutes, a number above 0");const d=await q(e,t,void 0,Yn(n)),u=await q(e,t,void 0,a),p=r(async f=>{const h=await $(u,null);if(!h?.started||h.done)return;const{waits:g,...k}=h;await y(u,f?{...k,waits:f}:k)},"mark"),w=Date.now()+c*6e4;await p(n);try{for(;;){const f=await $(d,null);if(f?.kind!==se&&l(`${t} has no helper named ${n}`),f.done){const g=f.writes&&f.worktree?[`branch ${f.worktree.branch} in ${f.worktree.path}: merge it with agent merge ${t} --name ${n}`]:[];return{lines:[`helper ${n} done: ${f.note??"no hand-back"}`,...g]}}const h=f.session?.pid??null;h&&!s(h)&&l(`helper ${n}'s process exited without builder done; do its task yourself`),Date.now()>=w&&l(`helper ${n} is still ${f.started?"running":f.writes?"waiting for an implementer slot":"waiting to launch"} after ${c} minutes; run agent wait again`),await new Promise(g=>setTimeout(g,o))}}finally{await p(null)}}r(sl,"waitHelper");async function rl(e,t,n="main"){return n==="main"?!1:(await $(await q(e,t,void 0,n),null))?.kind===se}r(rl,"isHelper");async function cl(e,t,n="main"){const a=await $(await q(e,t,void 0,n),null);return!!(a?.started&&!a.done&&Be(a))}r(cl,"isPlanner");async function dl(e,t,{file:n,title:a}){A(e);const i=S(a,"--title");(typeof n!="string"||!n.endsWith(".html"))&&l("--file takes the page, an .html file");const o=await P.readFile(m.resolve(n),"utf8").catch(()=>l(`--file ${n} can't be read`));o.trim()||l(`--file ${n} is empty`),await x(e,t);const{id:s}=I(t),c=m.join(e,"public","visuals",s);await P.mkdir(c,{recursive:!0});const d=i.toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,60).replace(/^-+|-+$/g,"")||"visual";let u=`${d}.html`;for(let w=2;fe(m.join(c,u));w++)u=`${d}-${w}.html`;await P.writeFile(m.join(c,u),o,{flag:"wx"});const p=`visuals/${s}/${u}`;return await U(e,t,{text:i,link:p,kind:we}),{lines:[p]}}r(dl,"addVisual");async function ll(e){const t=[];for(const n of await F(e)){const a=await $(n.file,null);for(const i of a?.items||[]){if(Y(i))continue;const o=await $(m.join(n.dir,"agents",`${i.id}.json`),null);!o?.started||o.done||!o.dispatched||Be(o)||t.push({key:`${n.slug}/${i.id}`,kind:ft(o.kind),model:o.model})}}return t}r(ll,"cutOffBuilds");async function xr(e,t,n){const{source:a,wave:i,item:o}=await x(e,t);for(const s of Object.keys(n))n[s]===void 0?delete o[s]:o[s]=n[s];await y(a.file,i)}r(xr,"restoreHold");async function vr(e,t,n){const{source:a,wave:i,item:o}=await x(e,t);for(const s of["lane","lane_at","closed_at","reopened_at"])n[s]===void 0?delete o[s]:o[s]=n[s];await y(a.file,i)}r(vr,"restoreLane");async function ul(e,t){return e||l("audit reads a board: pass --site <route>"),t!==void 0&&I(t),{lines:Ro(No(wa(e),{key:t}))}}r(ul,"auditLog");const _r=["codex","claude"];async function fl(e,t){const{item:n}=await x(e,t),{kind:a,model:i,reason:o}=gn({card:n});return{lines:[`${[a,i].filter(Boolean).join(" ")}: ${o}`]}}r(fl,"pickBuilder");async function Ki(e,t,{dir:n}={}){const{id:a}=I(t),i=m.dirname(await q(e,t,n)),o=await H(m.dirname(i),a);return(await Promise.all(o.map(c=>$(c,null)))).filter(c=>c?.worktree?.path).sort((c,d)=>String(d.started).localeCompare(String(c.started)))[0]?.worktree??null}r(Ki,"recordedWorktree");async function wl(e,t,n,{phase:a,dir:i,agent:o="main",eta:s,clearEta:c}){s!==void 0&&c&&l("pass --eta or --clear-eta, not both");const d=Wi(e,a,n,"builder progress"),u=await q(e,t,i,o),p=await $(u,null);(!p?.started||p.done)&&l(`builder progress needs an open builder record on ${t}; run builder start first`);const w=Ui(d,Be(p),"builder progress");return s!==void 0&&(p.eta=qi(s,"--eta")),c&&delete p.eta,(s!==void 0||c)&&await y(u,p),await Gn(e,t,o,p,w,new Date().toISOString()),u}r(wl,"builderProgress");async function pl(e,t){const{source:n}=await x(e,t),a=M(e,t);if((await G(a)).filter(s=>s.kind==="status").pop()?.phase==="landing")return;const o=(await Promise.all((await H(n.dir,I(t).id)).map(s=>$(s,null)))).filter(s=>s?.started&&Je(s)&&s.label!==la).map(s=>s.started).sort().pop();await z(a,{ts:new Date().toISOString(),who:"claude",kind:"status",phase:"landing",text:"Landing: rebasing onto main and landing",...o?{run:o}:{}})}r(pl,"landingLine");const We=r(e=>(e?.calls||[]).filter(t=>!e.verified||t.at>e.verified.at),"unverifiedCalls");async function hl(e,t,n,{dir:a,agent:i="main"}){const o=S(n,"builder call");/\n/.test(o)&&l("builder call takes one line; record each call on its own");const s=await q(e,t,a,i),c=await $(s,null);return(!c?.started||c.done)&&l(`builder call needs an open builder record on ${t}; run builder start first`),await y(s,{...c,calls:[...c.calls||[],{at:new Date().toISOString(),text:o}]}),s}r(hl,"recordCall");async function jt(e,t){const n=await G(M(e,t)),a=new Map(n.filter(i=>i.kind===It.answer).map(i=>[i.ask,i]));return n.filter(i=>i.kind===It.ask).map(i=>{const o=a.get(i.ask);return{id:i.ask,agent:i.agent||"main",text:i.text,at:i.ts,answer:o?{text:o.text,at:o.ts}:null,deferred:!!o?.deferred}})}r(jt,"builderAsks");const Sr=r(e=>ge().find(t=>je(t.dir,e))??{dir:e},"boardTarget");async function ml(e,t,n,{agent:a="main"}){A(e);const i=S(n,"builder ask");/\n/.test(i)&&l("builder ask takes one line; ask each question on its own");const o=await $(await q(e,t,void 0,a),null);(!o?.started||o.done)&&l(`builder ask needs an open builder record on ${t}; run builder start first`);const s=`ask${(await jt(e,t)).length+1}`;return await z(M(e,t),{ts:new Date().toISOString(),who:`${Bt(o)||o.kind||"Claude"} builder`,kind:It.ask,ask:s,agent:a,text:i}),await me(Sr(e),"builder-ask",{key:t,ask:s,agent:a,text:i}),{id:s,lines:[]}}r(ml,"askOrchestrator");async function gl(e,t,n,a,{deferred:i=!1}={}){A(e);const o=S(a,"--text"),s=(await jt(e,t)).find(d=>d.id===n)||l(`no ask "${n}" on ${t}; builder asks ${t} lists them`);s.answer&&l(`${n} on ${t} is already ${s.deferred?"deferred":"answered"}`);const c=new Date().toISOString();if(await z(M(e,t),{ts:c,who:"claude",kind:It.answer,ask:n,...i?{deferred:!0}:{},text:o}),i){const d=await q(e,t,void 0,s.agent),u=await $(d,null);u&&await y(d,{...u,calls:[...u.calls||[],{at:c,text:`Question: ${s.text} (deferred to the owner)`}]})}return{lines:[]}}r(gl,"answerBuilder");async function $l(e,t,{wait:n=!1,timeout:a=9,every:i=2e3}={}){A(e);const o=Number(a);o>0||l("--timeout takes minutes, a number above 0");const s=Date.now()+o*6e4;for(;;){const c=await jt(e,t);if(!c.length)return{lines:["no asks yet"]};const d=c.filter(u=>!u.answer);if(!n||!d.length)return{lines:c.flatMap(u=>[`${u.id}. ${u.text}`,` ${u.answer?`${u.deferred?"deferred":"answered"}: ${u.answer.text}`:"waiting on the orchestrator"}`])};Date.now()>=s&&l(`${d.map(u=>u.id).join(", ")} still waiting on the orchestrator after ${o} minutes; run builder asks ${t} --wait again`),await new Promise(u=>setTimeout(u,i))}}r($l,"listAsks");async function jr(e,t,n){const a=t.topic.startsWith("item:")&&!t.topic.startsWith("item:ask:")?t.topic.slice(5):l(`${n}.ask goes on a card's question, topic item:<wave>/<id>`),i=(await jt(e,a)).find(o=>o.id===t.ask)||l(`${n}.ask names no ask "${t.ask}" on ${a}; builder asks ${a} lists them`);i.deferred||l(`${n}.ask names ${i.id}, which the orchestrator ${i.answer?"answered":"has not deferred"}; only a deferred ask goes to the owner`)}r(jr,"checkAskQuestion");async function kl(e,t,{dir:n,agent:a="main"}){const i=await $(await q(e,t,n,a),null),o=i?.calls||[];if(!o.length)return{lines:["no judgment calls recorded"]};const{verified:s}=i,c=We(i).length?"not verified yet":`verified ${s.at}${s.note?`: ${s.note}`:""}`;return{lines:[...o.map((d,u)=>`${u+1}. ${d.text}`),c]}}r(kl,"listCalls");async function bl(e,t,{dir:n,agent:a="main",note:i}){const o=await q(e,t,n,a),s=await $(o,null);if(We(s).length||l(`${t} has no unverified judgment calls${a==="main"?"":` from agent ${a}`}; builder calls ${t} lists them`),e){const c=new Set((await $(L(e),{items:[]})).items?.filter(u=>u.topic===`item:${t}`&&u.ask).map(u=>u.ask)),d=(await jt(e,t)).find(u=>u.deferred&&u.agent===a&&!c.has(u.id));d&&l(`${d.id} on ${t} was deferred to the owner; add its question with "ask": "${d.id}" first`)}return await y(o,{...s,verified:{at:new Date().toISOString(),...i===void 0?{}:{note:S(i,"--note")}}}),o}r(bl,"verifyCalls");async function yl(e,t){const{source:n,item:a}=await x(e,t),i=r((s,c)=>[(s.options||[]).find(d=>d.id===c.choice)?.label,c.note].filter(Boolean).join(": ")||"answered","said"),o=(await de(e,t)).asked.filter(s=>s.ask&&s.answers.length).map(s=>({ask:s.ask,question:s.question,answer:i(s,s.answers.at(-1))}));return{key:t,ref:await K(e,t),title:a.title||t,notes:a.plan?m.join(n.dir,a.plan):null,thread:M(e,t),worktree:(await Ki(e,t))?.path??null,answers:o,links:await Or(e,t),rebasePR:a.rebase_run?.pr??null}}r(yl,"briefFacts");async function Or(e,t){const n=new Map;for(const c of await F(e))for(const d of(await $(c.file,null))?.items||[])n.set(`${c.slug}/${d.id}`,{entry:c,it:d});const a=I(t).wave,{it:i}=n.get(t),o=[...n].filter(([,c])=>(c.it.related||[]).some(d=>J(c.entry.slug,d)===t)).map(([c])=>c),s=[...i.parent?[["parent",J(a,i.parent)]]:[],...(i.deps||[]).map(c=>["depends on",J(a,c)]),...[...new Set([...(i.related||[]).map(c=>J(a,c)),...o])].map(c=>["related",c])].filter(([,c])=>n.has(c));return Promise.all(s.map(async([c,d])=>{const{entry:u,it:p}=n.get(d);return{relation:c,ref:await K(e,d),title:p.title||d,lane:Ke[p.lane||"todo"],notes:p.plan?m.join(u.dir,p.plan):null,thread:M(e,d)}}))}r(Or,"cardLinks");async function xl(e){const t=[];for(const n of await F(e)){const a=await $(n.file,null);for(const i of a?.items||[]){if(Y(i))continue;const o=(await Promise.all((await H(n.dir,i.id)).map(u=>$(u,null)))).filter(Boolean);if(!fn(o))continue;const s=o.map(u=>u.verified?.at).filter(Boolean).sort().at(-1);if(!s||o.some(u=>We(u).length))continue;const c=`${n.slug}/${i.id}`;if((await de(e,c)).asked.some(u=>u.state==="open")||await Gt(e,c,i))continue;const d=o.filter(u=>u.worktree?.path).sort((u,p)=>String(p.started).localeCompare(String(u.started)))[0];d&&fe(d.worktree.path)&&t.push({key:c,verified:s,kind:ft(d.kind),model:d.model})}}return t}r(xl,"landable");async function vl(e,t){const{source:n,item:a}=await x(e,t);(await Promise.all((await H(n.dir,a.id)).map(c=>$(c,null)))).some(c=>c?.started&&!c.done)||l(`${t} has no open builder; only a card whose lander stopped has one to relaunch`),await pe(e,t,{handBack:!0,next:"A lander relaunches to land the verified build"});const o=m.join(e,"data","landers.json"),s=await $(o,{});delete s[t],await P.writeFile(o+".tmp",JSON.stringify(s)+`
|
|
34
|
+
`),await P.rename(o+".tmp",o)}r(vl,"relaunchLander");async function _l(e,t,{dir:n}={}){const{id:a}=I(t),i=await H(m.dirname(m.dirname(await q(e,t,n))),a),o=[];for(const s of i){if(!We(await $(s,null)).length)continue;const c=m.basename(s,".json").slice(String(a).length+1)||"main";o.push(c==="main"?"":` --agent ${c}`)}o.length&&l(`land: ${t} has judgment calls nobody verified yet; check them with ${o.map(s=>`builder calls ${t}${s}`).join(", ")}, then run ${o.map(s=>`builder verified ${t}${s}`).join(", ")}`)}r(_l,"checkCallsVerified");async function Sl(e,t,{dir:n,agent:a="main",next:i}){const o=await $(await q(e,t,n,a),null),s=o&&!o.done?We(o):[];if(!s.length)return null;i!==void 0&&l(`builder done sets ${t}'s Next itself while its record has judgment calls; run it without --next`);const c=s.some(u=>u.text.startsWith("Question:"))?"Answer the builder's questions":"Verify the builder's judgment calls",d=await pe(e,t,{handBack:!0,next:c});return{lines:[`handed back: Next "${c}"`,...d.lines]}}r(Sl,"handBackCalls");async function jl(e,t,{agent:n="main"}){if(n!=="main")return!1;const a=await q(e,t,void 0,n),i=await $(a,null);if(!i?.started||i.done||!i.worktree?.path)return!1;const{id:o}=I(t);for(const s of await H(m.dirname(m.dirname(a)),o)){const c=s===a?null:await $(s,null);if(!c?.started||c.done||c.kind==="install")continue;const d=m.basename(s,".json").slice(String(o).length+1);l(`builder done would finish ${t} while agent ${d} is still open; run builder done ${t} --agent ${d} first`)}return!0}r(jl,"finishesCard");async function Ol(e,t){const{source:n,item:a}=await x(e,t);if(Y(a))return null;const i=(await Promise.all((await H(n.dir,a.id)).map(s=>$(s,null)))).filter(Boolean);if(!fn(i)||i.some(s=>We(s).length||s.started&&!s.done&&s.kind!=="install"))return null;const o=await Ki(e,t);return!o||!fe(o.path)?null:{waits:(await de(e,t)).asked.filter(s=>s.state==="open").map(s=>s.id)}}r(Ol,"verifiedFinish");async function Pl(e,t){const{item:n}=await x(e,t);if(n.reopened_at&&l(`the owner reopened ${await K(e,t)}; it closes on the owner's Done`),n.lane!=="done"||Oe(n).some(o=>o.merged_at))return;const a=String(n.closed_at||"");(await G(m.join(e,"data","events.jsonl"))).some(o=>o.kind==="item-message"&&o.data?.key===t&&String(o.ts)>a)&&l(`the owner reopened ${await K(e,t)} with a message; it closes on the owner's Done`)}r(Pl,"checkNotReopened");async function H(e,t){const n=m.join(e,"agents");return(await P.readdir(n).catch(()=>[])).filter(a=>a===`${t}.json`||a.startsWith(`${t}@`)&&!a.startsWith(`${t}@past-`)&&a.endsWith(".json")).map(a=>m.join(n,a))}r(H,"builderFiles");async function nn(e,t,n,{handedBack:a,endHandBack:i=!1,held:o=!1}={}){const s=new Date().toISOString();for(const c of await H(e,t)){const d=await $(c,null);if(!d)continue;if(d.done||d.kind==="install"&&!n){if(i&&d.handed_back){const{handed_back:f,...h}=d;await y(c,h)}continue}const{eta:u,handed_back:p,...w}=d;await y(c,{...w,done:s,...a?{handed_back:a}:{},...o&&d.started?{hold_stopped:!0}:{}})}}r(nn,"closeBuilders");async function Dl(e,t){const n=await q(e,t),a=await $(n,null);if(!a?.hold_stopped)return null;const{hold_stopped:i,...o}=a;return await y(n,o),{kind:a.kind??null,label:a.label??null}}r(Dl,"takeHoldStopped");async function Pr(e,t,{dir:n,agent:a,startedBetween:i,note:o,next:s,requireNext:c=!1}){const d=await q(e,t,n,a),u=await $(d,null);if(!u||u.done)return d;if(i){const _=Date.parse(u.started);if(!(_>=i[0]&&_<=i[1]))return d}const{eta:p,...w}=u,f=new Date().toISOString(),h={...w,done:f,...o?{note:o}:{}},{id:g}=I(t),k=m.dirname(m.dirname(d)),v=await Promise.all((await H(k,g)).map(_=>_===d?h:$(_,null))),D=!v.some(_=>_?.started&&!_.done),O=n?{file:m.join(k,"wave.json")}:(await x(e,t)).source,B=await $(O.file,null),N=B?.items?.find(_=>_.id===g);return s!==void 0&&(oi(N||l(`${t} has no item to set --next on`),s,await Un(e,t)),e&&await hi(e,t,N,N.next)),c&&N&&pn(N,v)&&l(`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(d,h),await nt(e,t,{screen:u.kind==="grill"}),o&&await Gn(e,t,a,u,{detail:o},f),s!==void 0&&await y(O.file,B),d}r(Pr,"stopBuilder");async function Dr(e){const t=[];for(const n of await F(e)){const a=await $(n.file,null);for(const i of a?.items||[])for(const o of await H(n.dir,i.id)){const s=await $(o,null);if(!s?.started||s.done)continue;const c=m.basename(o,".json");t.push({key:`${n.slug}/${i.id}`,agent:c.slice(String(i.id).length+1)||"main",label:s.label||"Builder",started:s.started})}}return t}r(Dr,"openBoardBuilders");async function Al(e){const t=await Dr(e);for(const n of t)await Pr(e,n.key,{agent:n.agent,note:"Board closed. Builder record stopped; work is kept for reopening."});return t}r(Al,"closeBoardBuilders");export{re as BoardError,xa as PUSH_LINE_MAX,es as SCREEN_AFTER,jn as UPLOAD_LIMIT,dc as WORKING_KINDS,Ud as abandonLaunch,Hd as abandonPlanner,vc as ackPing,_d as addItem,nd as addNotes,Jc as addPrototype,Zc as addQuestions,Ic as addReviewers,Wc as addRuleTo,Kc as addTopic,dl as addVisual,gl as answerBuilder,U as appendThread,Rd as archiveCards,Ad as archiveCount,Xt as archiveFile,en as archiveRank,ml as askOrchestrator,ud as askedGate,ul as auditLog,Sd as backfillTickets,Gt as blockWords,ie as boardConfig,oe as boardRepoOf,gd as breakOwnerCheck,yl as briefFacts,vd as builderModel,wl as builderProgress,Xd as bumpCard,Nc as cardLinked,K as cardName,qd as cardPR,Id as changeLineup,_l as checkCallsVerified,ld as checkClose,Pl as checkNotReopened,Zd as clearStall,Al as closeBoardBuilders,Ed as createLineup,ll as cutOffBuilds,Uc as editRuleIn,mc as eventCard,qc as fallBackFromLinear,_c as finishHandshake,Bd as finishLineup,jl as finishesCard,Sl as handBackCalls,yd as heldMerge,ol as helperOf,_a as hiddenFile,Md as implementAsIs,wd as isFinished,rl as isHelper,el as isLanding,cl as isPlanner,F as itemSources,Vd as landFirst,xl as landable,pl as landingLine,Bi as lineupNoteFile,$l as listAsks,kl as listCalls,Qc as listPrototypes,Mc as listRules,kc as markAsk,wc as markRead,Yd as markStalled,Qd as markStopped,lc as markWorking,bd as mergeCard,$i as mergedLine,jd as migrateFollowups,Pd as migratePhases,Od as migrateStatuses,bc as moveAsk,fd as moveCard,rd as moveQuestions,Kd as openAgents,Dr as openBoardBuilders,Jd as openRuns,Tc as opensPRs,I as parseKey,md as passOwnerCheck,fl as pickBuilder,hd as pickUp,od as planAgain,Ec as postDigest,xc as postIncomingReview,jc as postLink,kd as postPR,Oc as postReply,$d as prReport,Xc as publishScreened,ed as publishUnscreened,ls as pushLine,ws as pushUrl,Dc as queuePushes,Me as readArchive,De as readAskLinks,Ft as readAskProgress,gs as readAskTitles,Rn as readBoardRules,_s as readBoardSettings,La as readHandshake,hs as readHidden,$ as readJSON,Ld as readLineupNotes,ms as readMarks,ja as readMarksFile,tr as readNotes,Ut as readPRSlack,bt as readPrototypes,Qa as readScreening,hc as readUpload,as as readWorking,hl as recordCall,xi as recordOwnerTap,os as recordPush,Pc as recordReaction,Fd as recordRebase,yc as recordReview,zd as recordSession,Ki as recordedWorktree,gc as relateAsk,vl as relaunchLander,Yc as removePrototype,Bc as removeReviewers,Hc as removeRuleFrom,is as repliesSincePush,al as requestHelper,cd as resolveNoteAnswers,sd as resolveQuestion,xs as reviewDigest,Ac as reviewDue,Rc as reviewUpdated,zc as saveBoardRules,et as saveBoardSettings,Lc as saveBoardShare,pc as saveUpload,Wd as sendBackBumpedOut,uc as sendPush,Gc as servedRoundsOpen,Cc as setBoardPause,Fc as setBoardSetting,fc as setHidden,pe as setItem,pd as setMainInstall,Sc as setReviewers,Ss as settingsLines,be as settingsOf,td as settleAnswers,Vc as settleQuestion,rc as siteDir,Xo as sitesDir,ad as splitSharedNotes,Fs as stageImplement,Dd as stageImplementAsks,id as stageReply,St as startBuilder,il as startHelper,tl as startPlanner,Td as startRebase,Cd as startTappedBuild,nl as startVisual,Pr as stopBuilder,Gd as stopHeldMerge,Nd as stripDeadFields,Bn as takeFile,Dl as takeHoldStopped,xd as tapMerge,M as threadFile,ai as topicQuestions,$c as unfoldAsk,dd as unrecorded,Ol as verifiedFinish,bl as verifyCalls,sl as waitHelper,Ct as wavesDir,ds as withReplies,cc as writeReply};
|