@lovek4d/kickflip 0.5.3 → 0.6.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 +69 -37
- package/SKILL.md +19 -13
- package/bin/kickflip.mjs +12 -12
- package/bin/wait-events.mjs +2 -2
- package/board/agents.mjs +2 -2
- package/board/arch-review.mjs +4 -4
- package/board/bin/board.mjs +156 -83
- package/board/build-script.mjs +2 -1
- package/board/domain.mjs +9 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +9 -5
- package/board/onboarding.mjs +11 -0
- package/board/prototype-base.mjs +1 -0
- package/board/prototype-page.mjs +1 -0
- package/board/public/app-arch.js +1 -1
- package/board/public/app-cards.js +27 -13
- package/board/public/app-header.js +6 -7
- package/board/public/app-lanes.js +15 -38
- package/board/public/app-needs.js +36 -31
- package/board/public/app-snapshot.js +43 -70
- package/board/public/app-threads.js +38 -46
- package/board/public/app.html +9 -4
- package/board/public/app.js +78 -32
- package/board/public/hub-kit.js +62 -2
- package/board/public/hub.css +2 -0
- package/board/public/new-task.js +8 -6
- package/board/public/progress-review.html +46 -0
- package/board/public/progress-review.js +158 -0
- package/board/public/prototype-kit.js +109 -4
- package/board/public/prototype-switcher.js +123 -29
- package/board/public/prototype.html +57 -3
- package/board/public/router.js +7 -6
- package/board/public/shell.js +64 -24
- package/board/public/sidebar.css +1 -0
- package/board/public/sidebar.js +16 -69
- package/board/public/style.css +108 -57
- package/board/public/ui-cards.js +61 -46
- package/board/public/ui-enums.js +1 -1
- package/board/public/ui-header.js +25 -16
- package/board/public/ui-hub.js +12 -1
- package/board/public/ui-links.js +7 -7
- package/board/public/ui-page-header.js +19 -7
- package/board/public/ui-patch.js +7 -33
- package/board/public/ui-progress-bar.js +28 -0
- package/board/public/ui-questions.js +25 -17
- package/board/public/ui-sidebar.js +25 -23
- package/board/public/ui-snapshot.js +43 -52
- package/board/public/ui-threads.js +57 -28
- package/board/public/ui.js +92 -17
- package/board/public/view-hub-settings.js +12 -58
- package/board/public/view-hub.js +2 -1
- package/board/public/view-notes.js +27 -7
- package/board/public/view-questions.js +90 -68
- package/board/public/view-settings.js +133 -15
- package/board/render-check.mjs +10 -0
- package/board/review.mjs +1 -1
- package/board/server.mjs +19 -19
- package/board/stop.mjs +1 -0
- package/board/store.mjs +27 -18
- package/board/sync.mjs +2 -0
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +22 -21
- package/hooks-plugin/hooks/usage-gate.mjs +20 -3
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/front-door.mjs +1 -1
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/incoming.mjs +2 -2
- package/hub/owner-checks.mjs +1 -1
- package/hub/pr-report.mjs +2 -2
- package/hub/review-send.mjs +1 -1
- package/hub/sessions.mjs +2 -2
- package/hub/share.mjs +1 -1
- package/hub/slack.mjs +2 -2
- package/hub/try.mjs +7 -7
- package/hub/upgrade.mjs +1 -1
- package/hub/usage.mjs +3 -3
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/card-prs.mjs +1 -1
- package/shared/machine-gate.mjs +2 -0
- package/shared/prototype-frame.js +1 -0
- package/shared/usage-gate.mjs +1 -1
- package/board/public/ui-phase-bars.js +0 -17
package/board/store.mjs
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
|
-
var
|
|
2
|
-
`).filter(Boolean).map(t=>JSON.parse(t))}catch{return[]}},"readLines");async function
|
|
3
|
-
`):
|
|
4
|
-
`)})},"writeJSON");async function
|
|
5
|
-
`)}r(
|
|
6
|
-
`).filter(
|
|
7
|
-
`)),{exitCode:
|
|
8
|
-
`).map(
|
|
1
|
+
var Pi=Object.defineProperty;var r=(e,t)=>Pi(e,"name",{value:t,configurable:!0});import A from"node:fs/promises";import{constants as Ai,existsSync as me}from"node:fs";import $ from"node:path";import Xt from"node:os";import{execFile as Zn}from"node:child_process";import{fileURLToPath as Di}from"node:url";import{BUILD_REFUSED as Vn,BUILDER_ASK as xt,COMMANDS as Oe,LANES as vt,MOVE as Yn,handedBackAt as en,intakeKeyOf as Ri,MAX_IMPLEMENTERS as Ni,MAX_PLANNERS as Ei,builds as Xe,lineupPhases as tn,mergeBlockers as Li,modelName as _t,noTakePicked as Ii,ownerQuestion as Pe,plans as Ce,QUESTION_REOPEN as Bi,PLANNER_WORD as Ci,providerOf as et,owesNext as nn,pushTag as Ti,refOf as St,roundOf as an,roundSettledBy as qi,HELPER as ae,VISUAL as le,visualAgent as Fi}from"./domain.mjs";import{GITHUB_STATE as on,appendEvent as re,listBoards as de,samePath as he}from"../hub/boards.mjs";import{readUsageGate as Xn,pauseReason as Mi,pauseReasonFor as Wi,heldFor as Ui,holdsWork as Hi,builderPick as sn,pauseEnd as zi}from"../shared/usage-gate.mjs";import{reviseQueued as Ki,pushKindError as Ji,readClaudeModel as rn}from"../hub/push.mjs";import{AT_RE as Gi}from"../hub/duration.mjs";import{routeRecord as Z}from"../hub/routes.mjs";import{ticketSource as cn,ticketIn as ln,resolveTicket as Qi,ticketTarget as Zi,moveTicket as Vi,titleTickets as Yi,cardIdOf as Xi}from"./tickets.mjs";import{LINEAR_NEEDS_KEY as eo,linearStatus as to,linearWorks as no}from"../hub/linear.mjs";import{validateBranch as ao}from"../hub/repos.mjs";import{withFileLock as jt}from"../hub/locks.mjs";import{audit as io,auditLines as oo,boardAudit as ea,readAudit as so}from"../shared/audit.mjs";import{merge3 as ro}from"./merge3.mjs";import{fetchPR as ta,reportOf as na,githubLogin as co,postToPR as lo,postReview as uo,reviewsSince as fo,CLAUDE_PREFIX as tt,CLAUDE_MARK as dn}from"../hub/pr-report.mjs";import{allLanded as po,cardPRs as Ae,hasPR as Te,parsePRRef as un,prKeyOf as nt,prNames as qe,prURL as fn,storedPRs as Ot}from"../shared/card-prs.mjs";import{reviewLinks as wo,topicItems as mo,reviewersOf as pn}from"./review.mjs";import{readiness as aa}from"../hub/readiness.mjs";import{slackDay as ho}from"../hub/slack.mjs";import"../bin/install.mjs";import{readPermissions as go}from"./permissions.mjs";import{validBuildTarget as ia}from"./build-script.mjs";import{parseCheck as $o,checkText as ko}from"../shared/owner-check.mjs";import{RuleError as bo,rulesOf as oa,addRule as yo,editRule as xo,removeRule as vo,applyRules as _o,readHubRules as Pt,writeHubRules as So}from"../hub/rules.mjs";import{shareEntries as jo,validLogin as Oo}from"../hub/share.mjs";class ie extends Error{static{r(this,"BoardError")}}const d=r(e=>{throw new ie(e)},"fail"),Po=r(()=>$.join(Xt.homedir(),".claude","kickflip","sites"),"sitesDir"),At=r(()=>$.join(Xt.homedir(),".claude","waves"),"wavesDir"),sa=Symbol("read as"),g=r(async(e,t)=>{let n;try{n=await A.readFile(e,"utf8")}catch{return t}try{const a=JSON.parse(n);return a!==null&&typeof a=="object"&&!Array.isArray(a)?{...a,[sa]:n}:a}catch{return t}},"readJSON"),K=r(async e=>{try{return(await A.readFile(e,"utf8")).split(`
|
|
2
|
+
`).filter(Boolean).map(t=>JSON.parse(t))}catch{return[]}},"readLines");async function at(e,t){await A.mkdir($.dirname(e),{recursive:!0});const n=`${e}.${process.pid}.tmp`;await A.writeFile(n,t),await A.rename(n,e)}r(at,"replace");const y=r((e,t)=>{const{[sa]:n,...a}=t??{};return n===void 0?at(e,JSON.stringify(t,null,2)+`
|
|
3
|
+
`):jt(e,async()=>{const i=JSON.parse(n),o=JSON.parse(await A.readFile(e,"utf8").catch(()=>n));await at(e,JSON.stringify(ro(i,a,o),null,2)+`
|
|
4
|
+
`)})},"writeJSON");async function V(e,t){await A.mkdir($.dirname(e),{recursive:!0}),await A.appendFile(e,JSON.stringify(t)+`
|
|
5
|
+
`)}r(V,"appendLine");async function Dr(e){/^[\w-]+$/.test(e||"")||d(`--site takes a route such as ios-orchestrator, not "${e}"`);const t=$.join(Po(),e);return await A.stat(t).then(n=>n.isDirectory(),()=>!1)||d(`no site at ${t}`),t}r(Dr,"siteDir");const te=r(async e=>({title:"Board",items:"waves",...e?await g($.join(e,"board.json"),{}):{}}),"boardConfig");async function T(e){const t=await te(e);if(t.items==="waves")try{return(await A.readdir(At(),{withFileTypes:!0})).filter(a=>a.isDirectory()).map(a=>({slug:a.name,dir:$.join(At(),a.name),file:$.join(At(),a.name,"wave.json")}))}catch{return[]}const n=$.resolve(e,t.items);return[{slug:t.slug||$.basename(t.items,".json"),dir:$.dirname(n),file:n}]}r(T,"itemSources");const U=r((e,t)=>$.join(e,"data","threads",t.replace(/[^\w.-]+/g,"__")+".jsonl"),"threadFile"),L=r(e=>$.join(e,"data","owner.json"),"ownerFile"),it=r(e=>$.join(e,"data","staged-questions.json"),"stagedFile"),P=r(e=>e||d("this command needs --site <route>"),"needSite"),j=r((e,t)=>typeof e=="string"&&e.trim()?e.trim():d(`${t} must be non-empty text`),"text"),Y=r((e,t)=>e===void 0?void 0:t(e),"optional"),Ao=/^prototype-[\w-]+\.html(?:[?#]\S*)?$/,Do=/^visuals\/[\w-]+\/[\w-]+\.html$/,ra=r((e,t)=>typeof e=="string"&&(Ao.test(e)||Do.test(e)||/^https?:\/\/\S+$/.test(e))?e:d(`${t} must be an http(s) URL or a site page such as prototype-<topic>.html?q=<id>`),"pageUrl"),Dt=r((e,t)=>typeof e=="boolean"?e:d(`${t} must be true or false`),"bool"),ge=r((e,t)=>typeof e=="string"&&/^[\w.-]+$/.test(e)?e:d(`${t} must use only letters, digits, "_", "." and "-"`),"domId"),Fe=r((e,t,n)=>{(!e||typeof e!="object"||Array.isArray(e))&&d(`${n} must be a JSON object`);const a=Object.keys(e).filter(i=>!t.includes(i));a.length&&d(`${n} has unknown field ${a.map(i=>`"${i}"`).join(", ")}; allowed: ${t.join(", ")}`)},"onlyKeys");function N(e){const t=/^([^/\s]+)\/([^/\s]+)$/.exec(e||"");return t?{wave:t[1],id:t[2]}:d(`expected <wave>/<id>, not "${e}"`)}r(N,"parseKey");async function Me(e,t){const n=(await T(e)).find(i=>i.slug===t)||d(`this board has no wave "${t}"`),a=await g(n.file,null);return(!a||!Array.isArray(a.items))&&d(`${n.file} is not a wave file with items[]`),{source:n,wave:a}}r(Me,"loadWave");async function v(e,t){const{wave:n,id:a}=N(t);let i=await Me(e,n);!i.wave.items.some(s=>s.id===a)&&await Is(e,t)&&(i=await Me(e,n));const o=i.wave.items.find(s=>s.id===a)||d(`wave "${n}" has no item "${a}"`);return{...i,item:o}}r(v,"loadItem");async function Rr(e,t,n){P(e),ge(t,"the event id");const a=(await K($.join(e,"data","events.jsonl"))).find(u=>u.id===t);a||d(`no event "${t}" in this site's data/events.jsonl`);const i=$.join(e,"data","replies",`${t}.html`);await at(i,j(n,"the reply"));const o=a.kind==="ask"?`ask:${t}`:a.kind==="item-message"&&typeof a.data?.key=="string"?a.data.key:null,s=o?await da(e,o,Fo(n)):[],c=a.kind==="wave-triage"?["lineup","wave"].filter(u=>typeof a.data?.[u]=="string").map(u=>`${u}:${a.data[u]}`):[],l=a.kind==="ask"?`item:ask:${t}`:c[0];return l&&s.push(...await qa(e,l,"Acted on: replied")),{file:i,lines:s}}r(Rr,"writeReply");const ca=r(e=>$.join(e,"data","working.json"),"workingFile");async function Ro(e){return(await g(ca(e),{})).events||{}}r(Ro,"readWorking");const Nr=["ask","item-message","wave-triage","overnight-triage"];async function Er(e,t){if(!t.length)return;const n=new Date().toISOString(),a=await Ro(e);await y(ca(e),{events:{...a,...Object.fromEntries(t.map(i=>[i,Z(a,i)||n]))}})}r(Er,"markWorking");const wn=r(e=>$.join(e,"data","push-state.json"),"pushStateFile");async function No(e,t){P(e);const n=Date.parse((await g(wn(e),{})).last_push);if(Number.isNaN(n))return 0;const a=$.join(e,"data","replies"),i=(await A.readdir(a).catch(()=>[])).filter(s=>/\.(html|json)$/.test(s));return(await Promise.all(i.map(s=>A.stat($.join(a,s)).then(c=>c.mtimeMs,()=>0)))).filter(s=>s>n&&s<=t.getTime()).length}r(No,"repliesSincePush");async function Eo(e,t){return await y(wn(P(e)),{last_push:t.toISOString()}),wn(e)}r(Eo,"recordPush");const Lo=$.join($.dirname(Di(import.meta.url)),"..","bin","kickflip.mjs");function Io(){if(!process.env.KICKFLIP_PUSH_CMD)return[process.execPath,Lo,"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 ie("KICKFLIP_PUSH_CMD must be a JSON array of strings")}r(Io,"pushCommand");async function Lr(e,t){const n=Ji(t.kind);n&&d(n),t.title===void 0&&d("push needs --title");const a=new Date,i=Co(t.body,await No(e,a)),o={kind:t.kind,title:t.title,body:i||void 0,url:Mo(t.url),tag:t.tag},[s,...c]=Io(),l=[...c,...Object.entries(o).flatMap(([p,f])=>f===void 0?[]:[`--${p}`,f]),...t.urgent?["--urgent"]:[],...t.now?["--now"]:[]],u=await new Promise(p=>{Zn(s,l,{encoding:"utf8"},(f,m,h)=>p({error:f,said:`${m||""}${h||""}`.split(`
|
|
6
|
+
`).filter(k=>k.trim())}))}),w=u.error?typeof u.error.code=="number"?u.error.code:1:0;if(w!==0)throw Object.assign(new ie([...u.said,`kickflip push failed (${typeof u.error.code=="number"?`exit ${w}`:u.error.message}); the push was not recorded`].join(`
|
|
7
|
+
`)),{exitCode:w});return await Eo(e,a),{lines:u.said}}r(Lr,"sendPush");const Bo=/(?:^| · )(and \d+ repl(?:y|ies))$/,Co=r((e,t)=>[e,t&&`and ${t} ${t===1?"reply":"replies"}`].filter(Boolean).join(" \xB7 "),"withReplies"),la=160;function To(e){const t=String(e).split(`
|
|
8
|
+
`).map(n=>n.replace(/\s+/g," ").trim()).find(Boolean)||"";return t.length>la?`${t.slice(0,la-1).trimEnd()}\u2026`:t}r(To,"pushLine");const qo={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" "};function Fo(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,i,n)=>{const a=i&&parseInt(i.replace(/^x/i,""),/^x/i.test(i)?16:10);return i?a<=1114111?String.fromCodePoint(a):t:J(ns,n.toLowerCase())??t})}r(is,"htmlText");function as(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(as,"pushUrl");const ss=r((e,t)=>`/${h.basename(e)}/#item=${encodeURIComponent(t)}&reply`,"threadUrl");async function Dn(e,t,i){const n=ts(i);if(!n)return[];const a=Xi(t),s=await aa(a,o=>[n,Xa.exec(o)?.[1]].filter(Boolean).join(" \xB7 "),ss(e,t));return s===null?[]:[`queued push ${a} now reads: ${s}`]}r(Dn,"freshenPush");const An=r(e=>h.join(e,"data","hidden.json"),"hiddenFile"),os=r(async e=>new Set((await $(An(e),{events:[]})).events||[]),"readHidden");let Rn=Promise.resolve();function Wo(e,t,i){me(t,"the event id");const n=Rn.then(async()=>{const a=await os(e);i?a.add(t):a.delete(t),await y(An(e),{events:[...a]})});return Rn=n.catch(()=>{}),n}r(Wo,"setHidden");const Nn=r(e=>h.join(e,"data","read.json"),"readMarksFile"),rs=r(async e=>(await $(Nn(e),{})).cards||{},"readMarks");let En=Promise.resolve();function Uo(e,t,i={}){const n=En.then(async()=>{const a=await rs(e),s=new Date().toISOString(),o=r((d,f)=>String(f||"")>d?f:d,"later"),c=Object.fromEntries([...new Set(t)].sort().map(d=>[d,o(a[d]||s,i[d])]));JSON.stringify(c)!==JSON.stringify(a)&&await y(Nn(e),{cards:c})});return En=n.catch(()=>{}),n}r(Uo,"markRead");const Bt=20*1024*1024,Se=r(e=>h.join(e,"data","uploads"),"uploadsDir"),Ce=r((e,t)=>h.join(Se(e),".meta",`${t}.json`),"uploadMeta"),Ln=/^\d+-[\w.-]+$/,Cn=/^[\w.+-]+\/[\w.+-]+$/,In=r(()=>Object.assign(new Q(`a file can be at most ${Bt/1024/1024} MB`),{status:413}),"tooLarge");async function Ko(e,t,{name:i,type:n,length:a}){if(A(e),a>Bt)throw In();const s=String(i||"").trim().slice(0,200)||"file",o=h.basename(s).replace(/[^\w.-]+/g,"-").replace(/^[.-]+/,"").slice(-80)||"file",c=String(n||"").split(";")[0].trim().toLowerCase(),d=Cn.test(c)?c:"application/octet-stream";await N.mkdir(h.dirname(Ce(e,"x")),{recursive:!0});let f,p;for(let m=Date.now();!p;m++)f=`${m}-${o}`,p=await N.open(h.join(Se(e),f),"wx").catch(g=>g.code==="EEXIST"?null:Promise.reject(g));const l=h.join(Se(e),f);let w=0;try{for await(const m of t){if(w+=m.length,w>Bt)throw In();await p.write(m)}}catch(m){throw await p.close(),await N.rm(l,{force:!0}),m}return await p.close(),await y(Ce(e,f),{name:s,type:d,size:w,at:new Date().toISOString()}),{name:s,url:`uploads/${f}`,path:l,type:d,size:w}}r(Ko,"saveUpload");async function Ho(e,t){if(!Ln.test(t||""))return null;const i=h.join(Se(e),t),n=await N.stat(i).catch(()=>null);if(!n?.isFile())return null;const a=await $(Ce(e,t),{});return{path:i,name:a.name||t,type:Cn.test(a.type||"")?a.type:"application/octet-stream",size:n.size}}r(Ho,"readUpload");const Bn=["github","slack"];async function re(e,t,{text:i,link:n,log:a,who:s="claude",via:o,kind:c}){A(e),String(t).startsWith("ask:")?await Je(e,t.slice(4),"the ask"):await v(e,t),/^[^\n]{1,40}$/.test(s.trim())||u("--who takes claude, love or a reviewer's name"),o!==void 0&&!Bn.includes(o)&&u(`--via takes ${Bn.join(" or ")}`);const d={ts:new Date().toISOString(),who:s.trim(),text:j(i,"--text"),link:z(n,p=>jn(p,"--link")),log:a,via:o,kind:c},f=U(e,t);return await Y(f,d),{file:f,lines:d.who==="claude"&&!o?await Dn(e,t,d.text):[]}}r(re,"appendThread");const he=r(e=>h.join(e,"data","ask-links.json"),"askLinksFile");async function _e(e){return(await $(he(e),{})).links||{}}r(_e,"readAskLinks");async function X(e,t){const{tickets:i}=ge(await M(e)),n=await q(e),a=/^ask:(.+)$/.exec(t)?.[1];if(a===void 0){const o=/^([^/\s]+)\/([^/\s]+)$/.exec(t);if(!o)return t;const c=n.find(p=>p.slug===o[1]),d=c&&await $(c.file,null);if(!d)return o[2];const f=(d.items||[]).find(p=>p.id===o[2]);return tt(d,f||{id:o[2]},i)}const s=e?J(await _e(e),a):void 0;if(s)return X(e,s);for(const o of n){const c=await $(o.file,null),d=(Array.isArray(c?.items)?c.items:[]).find(f=>f.from_ask===a);if(d)return tt(c,d,i)}return`task ${a}`}r(X,"cardName");async function Jo(e,t){const i=(await G(h.join(e,"data","events.jsonl"))).find(n=>n.id===t);return i?.kind==="ask"?X(e,`ask:${t}`):typeof i?.data?.key=="string"?X(e,i.data.key):`the ${i?.kind} event`}r(Jo,"eventCard");async function zo(e,t,i,n){A(e),me(t,"the event id"),(await G(h.join(e,"data","events.jsonl"))).some(c=>c.id===t&&c.kind==="ask")||u(`no ask "${t}" in this site's data/events.jsonl`),await Tt(e,t),await v(e,i);const s={ts:new Date().toISOString(),who:"claude",text:j(n,"--summary")},o=await _e(e);return await y(he(e),{links:{...o,[t]:i}}),await Y(U(e,i),s),he(e)}r(zo,"relateAsk");async function Go(e,t){await Je(e,t,"the ask");const i=await _e(e);return J(i,t)===void 0&&u(`ask "${t}" is not folded into a card`),delete i[t],await y(he(e),{links:i}),he(e)}r(Go,"unfoldAsk");const lt=r(e=>h.join(e,"data","ask-progress.json"),"askProgressFile");async function ut(e){return(await $(lt(e),{})).asks||{}}r(ut,"readAskProgress");async function Tt(e,t){const i=J(await ut(e),t)?.moved;i&&u(`ask "${t}" was moved to /${i.route}; it lives there now`)}r(Tt,"refuseMoved");async function Qo(e,t,i,n){await Je(e,t,"the ask"),await Tt(e,t);const a=await ut(e);return await y(lt(e),{asks:{...a,[t]:{state:i,at:new Date().toISOString(),...n===void 0?{}:{next:j(n,"--next")}}}}),lt(e)}r(Qo,"markAsk");const Tn=[void 0,"waiting"];async function cs(e,t,i){const n=`ask "${t}"`;if(i.some(p=>p.kind==="ask-withdrawn"&&p.data?.id===t&&!p.data?.key))return{why:`${n} was deleted by the owner`};const a=J(await ut(e),t);if(a?.moved)return{why:`${n} was already moved to /${a.moved.route}`};const s=J(await _e(e),t);if(s)return{why:`${n} is folded into ${s}; run ask unfold ${t} first`};const o=[];for(const p of await q(e)){const l=await $(p.file,null);for(const w of Array.isArray(l?.items)?l.items:[])w.from_ask===t&&o.push({key:`${p.slug}/${w.id}`,it:w})}const c=o.find(({it:p})=>it(p)||!Tn.includes(p.status));return c?{why:`${n} became ${c.key}, which has started`}:o.length?{items:o.map(p=>p.key)}:(await Promise.all(["html","json"].map(p=>N.stat(h.join(e,"data","replies",`${t}.${p}`)).then(()=>!0,()=>!1)))).some(Boolean)||!!a||i.some(p=>p.kind==="ask-done"&&p.data?.id===t)||(await G(U(e,`ask:${t}`))).some(p=>(p.who||"claude")==="claude"&&p.kind!=="status")?{why:`${n} is not New: Claude already answered it`}:null}r(cs,"askNotNew");async function ds(e,t,i){const n=/^uploads\/([\w.-]+)$/.exec(i?.url||"")?.[1],a=Ln.test(n||"")?h.join(Se(e),n):null;if(!a||!await N.stat(a).then(d=>d.isFile(),()=>!1))return{name:String(i?.name||n||"file")};await N.mkdir(h.dirname(Ce(t,"x")),{recursive:!0});const s=n.replace(/^\d+-/,"");let o;for(let d=Date.now();o=`${d}-${s}`,!await N.copyFile(a,h.join(Se(t),o),Ji.COPYFILE_EXCL).then(()=>!0,p=>p.code==="EEXIST"?!1:Promise.reject(p));d++);const c=await $(Ce(e,n),null);return c&&await y(Ce(t,o),c),{...i,url:`uploads/${o}`,path:h.join(Se(t),o)}}r(ds,"copyUpload");async function Zo(e,t,i){A(e),me(t,"the event id"),(typeof i!="string"||!i.trim())&&u("ask move needs --to <route>, the board to move it to");const n=await G(h.join(e,"data","events.jsonl")),a=n.find(S=>S.id===t&&S.kind==="ask")||u(`no ask "${t}" in this site's data/events.jsonl`),s=we(),o=s.find(S=>S.route===i)||u(`--to ${i}: no board at /${i} in the hub's manifest; the boards are ${s.map(S=>S.route).join(", ")||"none"}`);ve(o.dir,e)&&u(`--to ${i}: that is this board`);const c=await cs(e,t,n);c?.why&&u(c.why);const d=n.filter(S=>S.kind==="ask-edited"&&S.data?.id===t&&typeof S.data?.text=="string").sort((S,_)=>String(S.ts).localeCompare(String(_.ts))),f=d.at(-1),p=[];for(const S of[...a.data?.files||[],...d.flatMap(_=>_.data.files||[])])p.push(await ds(e,o.dir,S));const l={route:h.basename(e),title:(await M(e)).title,id:t},w=f?f.data.text:a.data?.text||"",m=await pe(o,"ask",{text:w,...p.length?{files:p}:{},from:l}),g={route:i,title:o.title,id:m},b=new Date().toISOString(),P=await ut(e);await y(lt(e),{asks:{...P,[t]:{state:"done",at:b,moved:g}}});const D=`/${i}/#item=${encodeURIComponent(`ask:${m}`)}`;await Y(U(e,`ask:${t}`),{ts:b,who:"claude",text:`Moved to ${o.title}.`,link:D});for(const S of c?.items||[])await se(e,S,{status:"dropped",force:!0,text:`Dropped: its ask moved to ${o.title}.`});return{file:h.join(o.dir,"data","events.jsonl"),lines:[`moved to ${D}`,...(c?.items||[]).map(S=>`dropped: ${S}`)]}}r(Zo,"moveAsk");const qn=r(e=>h.join(e,"data","checked.json"),"checkedFile");async function Vo(e,t,{idle:i}={}){A(e),i&&t!==void 0&&u("pass --every or --idle, not both"),i===void 0&&t===void 0&&(i=!await ls(e));const n=new Date,a=i?null:t===void 0?la.away:/^[1-9]\d{0,3}$/.test(String(t))?Number(t):u("--every takes the minutes between checks, such as 30");return await y(qn(e),i?{at:n.toISOString(),every:null,idle:!0}:{at:n.toISOString(),every:a}),{file:qn(e),every:a,lines:await us()}}r(Vo,"writeChecked");async function Yo(e,t,{state:i,text:n}={}){return A(e),["posted","needs"].includes(i)||u("review record needs --state posted or --state needs"),String(n??"").trim()||u("review record needs the review text, as --text or --file"),Mn(e,await Fn(e,t,"review record"),{state:i,text:n})}r(Yo,"recordReview");async function Fn(e,t,i){/^[1-9]\d{0,6}$/.test(String(t))||u(`${i} takes the PR number, such as 437, not "${t}"`);const n=await $(h.join(e,"data","incoming.json"),{});return{...(n?.prs||[]).find(o=>o.number===Number(t))||(await $(h.join(e,"data","reviews.json"),{}))[t]||u(`PR #${t} is not an incoming PR on this board`),repo:n?.repo||ee(e,{})}}r(Fn,"incomingPR");async function Mn(e,{number:t,title:i,author:n,url:a,touches:s=[],asked:o=!1,head:c},{state:d,text:f,review_url:p}){const l=h.join(e,"data","reviews.json"),w=await $(l,{});return w[t]={number:Number(t),title:i,author:n,url:a,touches:s,asked:o,head:c,state:d,text:f,...p?{review_url:p}:{},at:new Date().toISOString()},await y(l,w),{file:l,lines:[]}}r(Mn,"saveReview");async function Xo(e,t,{words:i,readAt:n}){A(e);const a=await Fn(e,t,"review post"),s=String(i??"").trim()||u("review post needs the review text, as --file"),o=`${Te}Claude review from the ${(await M(e)).title} board`;(s.startsWith(Te.trim())||s.includes("Claude review from")||s.includes(Ct))&&u(`review post adds the ${Te.trim()} header and the claude-bridge marker itself; pass only your words`);const c=a.repo||u("review post needs a board on GitHub; this board has no repo there");Number.isNaN(Date.parse(n))&&u("--read-at takes the time you read the PR, such as 2026-11-05T09:30:00Z");const d=ge(await M(e)).github_login;try{const f=await Pa({repo:c,pr:a.number,since:n});f.length&&u(`PR #${t} got ${f.length===1?"a review":`${f.length} reviews`} after ${n} (${f.map(l=>`${l.login} at ${l.at}`).join(", ")}); reread the PR and review again`);const p=await Oa({repo:c,pr:a.number,head:a.head,body:`${o}
|
|
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}
|
|
11
11
|
|
|
12
|
-
${
|
|
12
|
+
${o}
|
|
13
13
|
|
|
14
|
-
${
|
|
15
|
-
`);
|
|
16
|
-
`)[0]}`));i(Object.fromEntries((JSON.parse(s).latestReviews||[]).map(c=>[c.author?.login,c.state])))})})}r(fs,"ghLatestReviews");async function or(e,t,i,{fetch:n,reviews:a=fs}={}){A(e);const s=Na({fetch:n})||u(La),o=j(i,"what changed"),{wave:c,item:d}=await v(e,t),f=at(c,d,t,st()),p=Fe(c,(await M(e)).reviewers).filter(m=>f[m.name]?.ts);if(!p.length)return{lines:[`no reviewer waiting on a sent link for ${t}`]};const l=await Promise.all(Ne(d,ee(e,c)).map(m=>a(m.repo,m.number))),w=p.filter(m=>!l.length||l.some(g=>g[m.github]!=="APPROVED"));for(const m of w)try{await s.post({channel:f[m.name].channel||m.dm,thread_ts:f[m.name].ts,text:`\u{1F916} Updated: ${o}`})}catch(g){throw g instanceof Ea&&u(`Updated to ${m.name} on ${t} not sent: ${g.message}`),g}return{lines:[w.length?`\u{1F916} Updated sent to ${w.map(m=>m.name).join(", ")} on ${t}`:`every reviewer on ${t} has approved; nothing sent`]}}r(or,"reviewUpdated");function Hn(e,t,i){const n=Ne(t,i).map(a=>({...a,url:Ra(a),gh:J(e.prs,Aa(a))||null}));return{prs:n,pr:n[Da(n.map(a=>a.gh))]||null}}r(Hn,"cachedPRs");async function ps(e,{now:t=new Date}={}){A(e);const i=await M(e),n=await $(pn,{}),a=st(),s=new Map;for(const p of await q(e)){const l=await $(p.file,{}),w=Fe(l,i.reviewers);for(const g of w)s.has(g.name)||s.set(g.name,{name:g.name,slack_user:g.slack_user,dm:g.dm,items:[]});const m=ee(e,l);for(const g of l.items||[]){if(["merged","done","dropped"].includes(g.status))continue;const b=`${p.slug}/${g.id}`,{pr:P}=Hn(n,g,m),D=P?.gh;if(D?.state!=="OPEN"||D.isDraft||D.failing?.length||D.reviewDecision==="APPROVED")continue;const S=at(l,g,b,a);for(const _ of w){const x=J(S,_.name),O=x?.ts?(t.getTime()-Number(x.ts)*1e3)/864e5:null;s.get(_.name).items.push({key:b,title:g.title,pr:P.number,url:P.url,sent_ts:x?.ts||null,...O>2?{days_old:Math.floor(O)}:{}})}}}const{date:o,morning:c}=vn(t),d=await $(Kn(e),{}),f=await xn.waiting("slack");return{date:o,due:!f&&c&&d.last_date!==o,...f?{waiting:f}:{},reviewers:[...s.values()].filter(p=>p.items.length)}}r(ps,"reviewDigest");async function rr(e,t,{ts:i,channel:n,items:a},{now:s=new Date}={}){const o=Un(i,n),c=[...new Set(j(a,"--items").split(",").map(p=>p.trim()))],d=await M(e);for(const p of c){const{wave:l}=await v(e,p);Wt(l,d.reviewers,t)}const f=Kn(e);return nt(f,async()=>{const p=await $(f,{});return await y(f,{last_date:vn(s).date,digests:[...p.digests||[],{reviewer:t,dm:o.channel,ts:o.ts,items:c,reactions:[]}]}),f})}r(rr,"reviewDigestSent");function cr(e,t){const i=Ft(Array.isArray(t)?t:[t],"the reviewer file");return Mt(e,n=>[...n.map(a=>i.find(s=>We(s.name,a.name))||a),...i.filter(a=>!n.some(s=>We(a.name,s.name)))])}r(cr,"addReviewers");function dr(e,t){return Mt(e,i=>{const n=t.filter(a=>!i.some(s=>We(s.name,a)));return n.length&&u(`this board has no reviewer ${n.map(a=>`"${a}"`).join(", ")}`),i.filter(a=>!t.some(s=>We(a.name,s)))})}r(dr,"removeReviewers");const ws=r(()=>h.join(Ot.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),Ut=r(e=>{try{return e()}catch(t){throw t instanceof Ta&&u(t.message),t}},"ruleCall"),Kt=r(async e=>Sn(await $(h.join(e,"board.json"),{})),"readBoardRules");let Jn=Promise.resolve();function pt(e,t){const i=Jn.then(async()=>{const n=h.join(A(e),"board.json"),a=await $(n,{}),{set:s,...o}=t(a);return await y(n,{...a,...s}),{file:n,...o}});return Jn=i.catch(()=>{}),i}r(pt,"writeBoardJSON");const zn=r((e,t)=>pt(e,i=>{const n=Ut(()=>t(Sn(i)));return{set:{rules:n.rules},...n}}),"writeBoardRules");function lr(e,t){const i=Object.keys(t||{}),[n]=i,a=typeof t?.[n]=="string"?t[n].trim():null;return(i.length!==1||!["add","remove"].includes(n)||!Ha(a))&&u("Send add or remove with one login, like name@github."),pt(e,s=>{const o=Ka(s),c=n==="remove"?o.filter(d=>d!==a):o.includes(a)?o:[...o,a];return{set:{share:c},share:c}}).then(({share:s})=>({share:s}))}r(lr,"saveBoardShare");const Gn=r(e=>typeof e=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e),"validGithubLogin"),Qn=r(e=>{try{return e?Et(e):null}catch{return null}},"storedTickets"),Ht=r(e=>Number.isInteger(e)&&e>=1&&e<=99,"validCap"),Jt=["max_implementers","max_planners"],ge=r(e=>{const t=Qn(e?.tickets);return{auto_merge:e?.auto_merge===!0,auto_build:e?.auto_build===!0,auto_pull:e?.auto_pull===!0,max_implementers:Ht(e?.max_implementers)?e.max_implementers:Gi,max_planners:Ht(e?.max_planners)?e.max_planners:Qi,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:e?.prs!==!1,...t?{tickets:t}:{},...Gn(e?.github_login)?{github_login:e.github_login}:{}}},"settingsOf");async function ms(e){return ge(await $(h.join(A(e),"board.json"),{}))}r(ms,"readBoardSettings");const ur=r((e,t)=>!!e?.github&&!e.local&&t.prs,"opensPRs");async function Ue(e,t){Ee(t,["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","tickets","github_login"],"the settings"),Object.keys(t).length||u("the settings name nothing to change"),t={...t};for(const[i,n]of Object.entries(t))if(i==="tickets"){try{t.tickets=Et(n)}catch(s){u(s.message)}t.tickets.source==="linear"&&!ka()&&u(ga);const a=t.tickets.source==="github"&&e?we().find(s=>ve(s.dir,e)):null;a&&!a.github&&u("GitHub issues need the board\u2019s repo on GitHub, and this board\u2019s repo isn\u2019t; pick internal or linear")}else i==="github_login"?n===""||n===null?t.github_login=void 0:Gn(n)||u("github_login must be a GitHub login"):Jt.includes(i)?Ht(n)||u(`${i} must be a whole number from 1 to 99`):dt(n,i);if(t.prs===!1){const i=e?we().find(n=>ve(n.dir,e)):null;i?.github&&!i.local&&!(await Ca(i.owner?.repo)).pushMain&&u("Merge lands each card on main and pushes it, so turn on Push main after a land first")}return pt(e,i=>{const n=ge(i),a=ge({...i,...t});return{set:t,settings:a,changed:Object.keys(t).filter(s=>JSON.stringify(n[s])!==JSON.stringify(a[s]))}})}r(Ue,"saveBoardSettings");async function fr(){if($a().set)return[];const e=[];for(const t of we()){if((await ms(t.dir)).tickets?.source!=="linear")continue;const{settings:i}=await Ue(t.dir,{tickets:{source:"internal"}});await pe(t,"settings",{tickets:i.tickets}),e.push(`/${t.route} fell back to internal tickets: Hub has no Linear key`)}return e}r(fr,"fallBackFromLinear");async function pr(e,t,i){const n=String(t).replace(/-/g,"_"),a=["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","github_login","tickets"];if(a.includes(n)||u(`no board setting "${t}"; settings are ${a.map(c=>c.replace(/_/g,"-")).join(", ")}`),n==="tickets"){let c;try{c=JSON.parse(i)}catch{u('settings set tickets takes the source as JSON, such as {"source":"github"}')}const{file:d,settings:f}=await Ue(e,{tickets:c});return{file:d,lines:hs({tickets:f.tickets})}}if(n==="github_login"){const{file:c,settings:d}=await Ue(e,{github_login:i});return{file:c,lines:[`github-login ${d.github_login||"unset"}`]}}if(Jt.includes(n)){const{file:c,settings:d}=await Ue(e,{[n]:/^\d+$/.test(i)?Number(i):i});return{file:c,lines:[`${t} ${d[n]}`]}}["on","off"].includes(i)||u(`settings set ${t} takes on or off, not "${i}"`);const{file:s,settings:o}=await Ue(e,{[n]:i==="on"});return{file:s,lines:[`${t} ${o[n]?"on":"off"}`]}}r(pr,"setBoardSetting");const hs=r(e=>Object.entries(e).map(([t,i])=>`setting ${t.replace(/_/g,"-")} ${t==="tickets"?JSON.stringify(i):t==="github_login"||Jt.includes(t)?i:i?"on":"off"}`),"settingsLines");async function zt(e,t,i){const n=t?Ut(()=>i(ot())):await zn(e,i);return t&&Ut(()=>Ua(n.rules)),{file:t?ws():n.file,lines:[`id ${n.rule.id}`]}}r(zt,"changeRules");async function Zn(e,t,i){const n=r(o=>o.map(c=>c.id),"ids");i||A(e);const[a,s]=i?[n(ot()),e?n(await Kt(e)):[]]:[n(await Kt(e)),n(ot())];a.includes(t)||u(`no ${i?"hub":"board"} rule "${t}"${s.includes(t)?`; it is a ${i?"board":"hub"} rule, so ${i?"drop":"pass"} --hub`:""}`)}r(Zn,"checkRuleId");async function wr(e){const t=r(i=>n=>`${i} ${n.id} ${n.by==="claude"?"claude":"owner"} ${n.text}`,"line");return{lines:[...ot().map(t("hub")),...e?(await Kt(e)).map(t("board")):[]]}}r(wr,"listRules");function mr(e,{text:t,hub:i=!1,by:n}){return i||A(e),zt(e,i,a=>qa(a,{text:t,by:n}))}r(mr,"addRuleTo");async function hr(e,t,{text:i,hub:n=!1}){return await Zn(e,t,n),zt(e,n,a=>Fa(a,t,i))}r(hr,"editRuleIn");async function gr(e,t,{hub:i=!1}){return await Zn(e,t,i),zt(e,i,n=>Ma(n,t))}r(gr,"removeRuleFrom");const $r=r((e,t,i)=>zn(e,n=>Wa(n,t,i)),"saveBoardRules");async function kr(e,{id:t,title:i,summary:n}){A(e);const a=await $(L(e),{topics:[],items:[]}),s=a.topics||[];return s.some(o=>o.id===j(t,"the topic id"))&&u(`topic "${t}" already exists`),s.push({id:t.trim(),title:j(i,"--title"),...n===void 0?{}:{summary:j(n,"--summary")}}),await y(L(e),{...a,topics:s,items:a.items||[]}),L(e)}r(kr,"addTopic");const Vn=["quiet","info","notice","danger","success"];function gs(e,t){if(e.diagram===void 0!=(e.practice===void 0)&&u(`${t} needs diagram and practice together`),e.diagram===void 0)return;const i=e.diagram;(!Array.isArray(i)||!i.length)&&u(`${t}.diagram must be a non-empty array of rows`),i.forEach((n,a)=>{(!Array.isArray(n)||!n.length||n.length>4)&&u(`${t}.diagram[${a}] must be a row of 1 to 4 boxes`),n.forEach((s,o)=>{const c=`${t}.diagram[${a}][${o}]`;Ee(s,["text","tone","to"],c),j(s.text,`${c}.text`),z(s.tone,d=>Vn.includes(d)?d:u(`${c}.tone takes ${Vn.join(", ")}`)),z(s.to,d=>Array.isArray(d)&&a+1<i.length&&d.every(f=>Number.isInteger(f)&&f>=0&&f<i[a+1].length)?d:u(`${c}.to must list boxes of the next row by index`))})}),(!Array.isArray(e.practice)||!e.practice.length)&&u(`${t}.practice must be a non-empty array of lines`),e.practice.forEach((n,a)=>j(n,`${t}.practice[${a}]`))}r(gs,"checkDiagram");function $s(e,t){(!Array.isArray(e)||!e.length)&&u(`${t}.options must be a non-empty array`);const i=new Set;e.forEach((n,a)=>{const s=`${t}.options[${a}]`;Ee(n,["id","label","recommended","detail","diagram","practice"],s),gs(n,s),i.has(j(n.id,`${s}.id`))&&u(`${t} has two options with id "${n.id}"`),i.add(n.id),j(n.label,`${s}.label`).startsWith(`${n.id}. `)&&u(`${s}.label starts with "${n.id}. ", but the page adds the option letter itself`),z(n.recommended,o=>dt(o,`${s}.recommended`)),z(n.detail,o=>typeof o=="string"?o:u(`${s}.detail must be text`))}),e.filter(n=>n.recommended).length>1&&u(`${t} recommends more than one option`)}r($s,"checkOptions");function ks(e,t,i){Ee(e,["option","card","answer"],i),t.some(n=>n.id===j(e.option,`${i}.option`))||u(`${i}.option names no option "${e.option}"`),j(e.card,`${i}.card`),j(e.answer,`${i}.answer`)}r(ks,"checkSuggested");const Ke=r(e=>h.join(e,"data","prototypes.json"),"prototypesFile"),Gt=r((e,t,i)=>h.join(e,"public","prototypes",t,`${i}.js`),"takeFile");async function wt(e){const t=e?(await $(Ke(e),{})).prototypes:null;return Array.isArray(t)?t:[]}r(wt,"readPrototypes");const ys=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:u(`a prototype topic uses only a-z, 0-9 and "-", not "${e}"`),"prototypeTopic");function bs(e){const t=/^([^:]*):(.*)$/s.exec(e);(!t||!/^[A-Z]$/.test(t[1]))&&u(`--take takes KEY:"Label" with KEY one capital letter, not "${e}"`);const i=j(t[2],`--take ${t[1]}'s label`);return/^[A-Z]\.\s/.test(i)&&u(`--take ${t[1]}'s label starts with "${i.slice(0,3)}"; the take pill shows the label alone, so drop the letter`),{key:t[1],label:i}}r(bs,"prototypeTake");const Ie=r(e=>/^prototype-(.+)\.html$/.exec(h.posix.basename(String(e||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function yr(e,{topic:t,title:i,card:n,takes:a=[]}){A(e),ys(t),j(i,"--title"),a.length||u('prototype add needs a --take KEY:"Label" for each take');const s=a.map(bs),o=s.find((f,p)=>s.findIndex(l=>l.key===f.key)!==p);o&&u(`--take ${o.key} is given twice`),n!==void 0&&await v(e,n);const c=await wt(e);c.some(f=>f.topic===t)&&u(`prototype ${t} is already recorded; run prototype rm ${t} first, or pick another topic`),await N.stat(h.join(e,"public",`prototype-${t}.html`)).then(()=>!0,()=>!1)&&u(`public/prototype-${t}.html is a hand-written page that questions may link; pick another topic`);const d={topic:t,title:i.trim(),...n===void 0?{}:{card:n},takes:s,at:new Date().toISOString()};return await y(Ke(e),{prototypes:[...c,d]}),{file:Ke(e),lines:[`page prototype-${t}.html`,...s.map(f=>`take ${f.key} ${Gt(e,t,f.key)}`)]}}r(yr,"addPrototype");async function br(e){A(e);const t=await wt(e);return{lines:t.length?t.map(i=>`${i.topic} \xB7 ${i.title} \xB7 ${i.takes.map(n=>`${n.key}: ${n.label}`).join(", ")}${i.card?` \xB7 ${i.card}`:""}`):["no prototypes recorded"]}}r(br,"listPrototypes");async function xr(e,t){A(e);const i=await wt(e);i.some(s=>s.topic===t)||u(`no prototype "${t}" is recorded`);const a=((await $(L(e),{items:[]})).items||[]).filter(s=>!s.resolved&&Ie(s.url)===t).map(s=>s.id);return a.length&&u(`prototype ${t} backs open question ${a.join(", ")}; resolve ${a.length===1?"it":"them"} first`),await y(Ke(e),{prototypes:i.filter(s=>s.topic!==t)}),Ke(e)}r(xr,"removePrototype");async function xs(e,t,i){const n=t.url?.split(/[?#]/)[0];if(!n||!h.posix.basename(n).startsWith("prototype-"))return;const a=h.posix.basename(n),s=t.url.split("#")[0].split("?").slice(1).join("?"),o=`${i}.url`;new URLSearchParams(s).get("q")!==t.id&&u(`${o} must be ${a}?q=${t.id} so the pill can pick`);const c=Ie(n),d=(await wt(e)).find(w=>w.topic===c);d||u(`${o} names ${a}, which is not a recorded prototype; record it with board.mjs prototype add ${c} --title "\u2026" --take A:"\u2026", then write its take files`);const f=d.takes.map(w=>w.key),p=t.options.map(w=>w.id),l=p.map(w=>f.indexOf(w));l.some((w,m)=>w<0||m>0&&w<l[m-1])&&u(`${i}.options ids ${p.join(", ")} must be a non-empty subset of ${a}'s takes, in take order: ${f.join(", ")}`);for(const w of f)await N.stat(Gt(e,c,w)).then(m=>m.isFile(),()=>!1)||u(`${o} take ${w} has no file yet; write ${Gt(e,c,w)}`)}r(xs,"checkPrototype");async function vr(e,t){A(e);const i=Array.isArray(t)?t:[t];i.length||u("the question file is empty");const n=await $(L(e),{topics:[],items:[]}),a=n.items||[],s=await $(Me(e),{}),o=new Set([...a,...Object.values(s).flat()].map(p=>p.id)),c=new Set((n.topics||[]).map(p=>p.id));for(const[p,l]of i.entries()){const w=`question ${l?.id??p+1}`;Ee(l,["id","topic","source","question","context","url","urgent","options","round_of","reask_of","ask","suggested","prototype_gate"],w),o.has(me(l.id,`${w}.id`))&&u(`question id "${l.id}" is already used`),o.add(l.id),j(l.topic,`${w}.topic`),l.topic.startsWith("item:ask:")?await Je(e,l.topic.slice(9),`${w}.topic`):l.topic.startsWith("item:")?await v(e,l.topic.slice(5)):l.topic.startsWith("lineup:")?await xt(e,l.topic.slice(7)):l.topic.startsWith("wave:")?await Us(e,l.topic.slice(5)):c.has(l.topic)||u(`topic "${l.topic}" is not declared; add it with: topic add ${l.topic} --title <t>`),j(l.question,`${w}.question`);for(const m of["source","context"])z(l[m],g=>j(g,`${w}.${m}`));z(l.url,m=>jn(m,`${w}.url`)),z(l.urgent,m=>dt(m,`${w}.urgent`)),$s(l.options,w),z(l.prototype_gate,m=>vs(m,l,w)),z(l.suggested,m=>ks(m,l.options,`${w}.suggested`)),await xs(e,l,w),l.ask!==void 0&&await eo(e,l,w),z(l.round_of,m=>{const g=a.find(b=>b.id===m);g||u(`${w}.round_of names no question "${m}" in owner.json`),g.topic!==l.topic&&u(`${w}.round_of names ${m} on topic ${g.topic}; a round stays on its question's topic, ${l.topic}`),Ie(l.url)||u(`${w}.round_of needs a url on a recorded prototype, prototype-<topic>.html?q=${l.id}`)}),z(l.reask_of,m=>{const g=a.find(b=>b.id===m);g||u(`${w}.reask_of names no question "${m}" in owner.json`),g.topic!==l.topic&&u(`${w}.reask_of names ${m} on topic ${g.topic}; a re-ask stays on its question's topic, ${l.topic}`),g.resolved||u(`${w}.reask_of names ${m}, which is not resolved; only a resolved question is asked again`)})}const d=[],f=[];for(const p of i){const l=p.topic.startsWith("item:")&&!p.topic.startsWith("item:ask:")?p.topic.slice(5):null,w=l&&await Yn(e,l);w&&((s[l]||=[]).push(p),f.push(p),d.push(`staged: ${p.id} waits for ${l}'s ${w} to finish; the card's staged questions then publish together`))}return f.length&&await y(Me(e),s),f.length===i.length?{file:Me(e),lines:d,staged:f.length}:Xn(e,n,a,i.filter(p=>!f.includes(p)),d)}r(vr,"addQuestions");async function Yn(e,t){const{source:i}=await v(e,t);for(const n of await te(i.dir,C(t).id)){const a=await $(n,null);if(a?.started&&!a.done&&Re(a))return a.kind}return null}r(Yn,"runningPlanner");async function mt(e,t){if(!e||await Yn(e,t))return;const i=await $(Me(e),{});if(!i[t])return;const{[t]:n,...a}=i,s=await $(L(e),{topics:[],items:[]});await Xn(e,s,s.items||[],n,[]),await y(Me(e),a)}r(mt,"publishStaged");async function Xn(e,t,i,n,a){const{answers:s,requests:o}=await He(e),c=new Date().toISOString(),d=[],f=[];for(const l of n.filter(w=>w.round_of))l.round=Nt(i.find(w=>w.id===l.round_of))+1,$e(t,[l.round_of],`Followed by prototype round ${l.round}`,c),f.push(l.round_of);for(const l of n.filter(w=>w.reask_of))$e(t,[l.reask_of],`Asked again as ${l.id}`,c),f.push(l.reask_of);for(const l of n){const w=Ie(l.url),m=i.filter(b=>b.topic===l.topic).map(b=>Ae(b,s,o)),g=l.topic.startsWith("item:")?ei(m):[];for(const b of m)g.includes(b.id)?($e(t,[b.id],`Followed by ${l.id}`,c),f.push(b.id)):!b.resolved&&w&&Ie(b.url)===w&&($e(t,[b.id],`Replaced by ${l.id}`,c),d.push(b.id))}const p={...t,topics:t.topics||[],items:[...i,...n.map(l=>({...l,asked_at:c}))]};return await y(L(e),p),{file:L(e),lines:[...a,...d.map(l=>`replaced: ${l}`),...f.map(l=>`resolved: ${l}`)]}}r(Xn,"publishQuestions");function vs(e,t,i){dt(e,`${i}.prototype_gate`),e&&((!t.topic.startsWith("item:")||t.topic.startsWith("item:ask:"))&&u(`${i}.prototype_gate goes on a card's topic, item:<wave>/<id>`),t.options?.map(n=>n.id).join()!=="yes,no"&&u(`${i}.prototype_gate takes exactly the options yes and no, in that order`))}r(vs,"checkGate");async function Sr(e,t){const i=await $(L(e),{topics:[],items:[]}),n=new Map((i.items||[]).map(o=>[o.id,o])),a=new Set(t.map(o=>n.get(String(o.id))?.topic).filter(o=>o?.startsWith("item:"))),s=[];for(const o of t){const c=n.get(String(o.id));if(!c?.prototype_gate||!["yes","no"].includes(o.choice)||($e(i,[c.id],`Prototype round: ${o.choice}. The board moved the card on.`),await y(L(e),i),o.choice==="no"))continue;a.delete(c.topic);const d=c.topic.slice(5);(await v(e,d)).item.state==="grilling"&&(await se(e,d,{state:"prototyping"}),await re(e,d,{text:"You asked for a prototype round, so the board moved the card to prototyping."}),s.push(d))}for(const o of a){const{asked:c}=await Zt(e,o),d=c.find(p=>p.prototype_gate&&p.answers.at(-1)?.choice==="no"),f=o.slice(5);if(!(!d||c.some(p=>p.state==="open")||(await v(e,f)).item.state!=="grilling"))try{await se(e,f,{state:"ready"}),await re(e,f,{text:"No prototype round, and nothing left to answer, so the board moved the card to ready."})}catch(p){if(!(p instanceof Q))throw p;await re(e,f,{text:`No prototype round, but the card isn't ready yet: ${p.message}`})}}return{prototype:s}}r(Sr,"settleGates");function ei(e){return e.filter(t=>t.state==="answered").map(t=>t.id)}r(ei,"answeredOnClaude");function $e(e,t,i,n=new Date().toISOString()){for(const a of e.items||[])t.includes(a.id)&&Object.assign(a,{resolved:i,resolved_at:n})}r($e,"setResolved");async function Qt(e,t,i,n){return $e(t,i,n),await y(L(e),t),L(e)}r(Qt,"markResolved");async function _r(e,t,i){A(e);const n=await $(L(e),{topics:[],items:[]}),a=(n.items||[]).find(p=>p.id===t);a||u(`no question "${t}" in owner.json`);const{answers:s,requests:o}=await He(e),c=Ae(a,s,o);if(c.state==="resolved")return L(e);c.state==="requested"&&u(`${t} waits on the prototype the owner asked for; question add with round_of ${t} resolves it`);const d=j(i,"--note");Ie(a.url)&&Ss(n,a,s);const f=await Qt(e,n,[t],d);return await ai(e,a.topic),f}r(_r,"resolveQuestion");async function jr(e,t,i,n=[]){A(e);const[a,...s]=[...new Set(t)];a||u("question move takes the ids of the questions to merge"),await v(e,i);const o=[i,...n];new Set(o).size!==o.length&&u("question move names a card twice");for(const l of n)await v(e,l);const c=await $(L(e),{topics:[],items:[]}),{answers:d,requests:f}=await He(e);for(const l of[a,...s]){const w=(c.items||[]).find(g=>g.id===l)||u(`no question "${l}" in owner.json`),{state:m}=Ae(w,d,f);m!=="open"&&u(`${l} is ${m}; question move takes open questions`)}for(const[l,w]of n.entries()){const{item:m}=await v(e,w),g=[...m.deps||[],o[l]].join(",");await se(e,w,{deps:g,relate:i})}const p=await X(e,i);return Object.assign(c.items.find(l=>l.id===a),{topic:`item:${i}`}),$e(c,s,`Duplicate of ${p}'s question ${a}`),await y(L(e),c),L(e)}r(jr,"moveQuestions");async function Or(e,t){const i=t.filter(({choice:a,note:s})=>!a&&String(s||"").trim()).map(a=>a.id),n=i.length?await $(L(e),{topics:[],items:[]}):null;n?.items?.some(a=>i.includes(a.id))&&($e(n,i,"Answered with a note"),await y(L(e),n))}r(Or,"resolveNoteAnswers");function Ss(e,t,i){const n=Nt(t),a=e.items.filter(s=>s.topic===t.topic&&Nt(s)===n).map(s=>Ae(s,i));ea(a,n)||u(`${t.id} is in prototype round ${n}, which no pick has settled yet; a pick settles it`)}r(Ss,"roundSettled");async function He(e){const t=await G(h.join(e,"data","events.jsonl"));return{answers:await Promise.all(t.filter(n=>n.kind==="owner-answer").map(async n=>{const a=await Promise.all(["html","json"].map(s=>N.stat(h.join(e,"data","replies",`${n.id}.${s}`)).then(o=>o.isFile(),()=>!1)));return{...n,reply:n.reply||a.some(Boolean)}})),requests:t.filter(n=>n.kind==="prototype-request")}}r(He,"ownerEvents");async function Zt(e,t){const i=await $(L(e),{topics:[],items:[]}),{answers:n,requests:a}=await He(e),s=(i.items||[]).filter(o=>o.topic===t).map(o=>Ae(o,n,a));return{owner:i,asked:s}}r(Zt,"topicQuestions");const ke=r((e,t)=>Zt(e,`item:${t}`),"cardQuestions"),Vt=r((e,t,i)=>ti(e,`item:${t}`,i),"resolveAnswered");async function ti(e,t,i){if(!e)return[];const{owner:n,asked:a}=await Zt(e,t),s=ei(a);return s.length&&await Qt(e,n,s,i),await ai(e,t),s.map(o=>`resolved: ${o}`)}r(ti,"resolveAnsweredOn");const ni=r(async(e,t)=>!!e&&(await ke(e,t)).asked.some(i=>i.state==="open"),"hasOpenQuestion");function ii(e,t,i){e.next=j(t,"--next"),i?e.next_on_questions=!0:delete e.next_on_questions}r(ii,"setNext");async function ai(e,t){const i=t.startsWith("item:")&&!t.startsWith("item:ask:")?t.slice(5):null;if(!i)return;let n;try{n=await v(e,i)}catch(c){if(c instanceof Q)return;throw c}const{source:a,wave:s,item:o}=n;o.next_on_questions&&((await ke(e,i)).asked.some(c=>c.state!=="resolved")||(delete o.next,delete o.next_on_questions,await y(a.file,s)))}r(ai,"clearWaitedNext");async function _s(e,t){const{source:i,wave:n,item:a}=await v(e,t);a.next===void 0&&a.next_on_questions===void 0||(delete a.next,delete a.next_on_questions,await y(i.file,n))}r(_s,"clearNext");async function si(e,t,i,n){if(!e)return async()=>[];const{owner:a,asked:s}=await ke(e,t),o=s.filter(d=>d.state==="open").map(d=>d.id);o.length&&!n&&u(`${i}: ${t} has ${o.length===1?"an open question":"open questions"} ${o.join(", ")} with no answer yet; pass --force to close the card and resolve ${o.length===1?"it":"them"} too`);const c=s.filter(d=>d.state!=="resolved").map(d=>d.id);return async()=>(c.length&&await Qt(e,a,c,"Closed with the card"),c.map(d=>`resolved: ${d}`))}r(si,"closeQuestions");async function Pr(e,t,i,n,a){await v(e,t),j(i,"--text"),z(n,s=>j(s,"--owner-step")),z(a,s=>gi(s)),await si(A(e),t,"land",!1)}r(Pr,"checkClose");async function oi(e,t){const i=await $(L(e),{items:[]}),{answers:n,requests:a}=await He(e);return Vi((i.items||[]).map(s=>Ae(s,n,a)),t)}r(oi,"unpickedPrototype");const js=r(e=>e.length>120?`${e.slice(0,119)}\u2026`:e,"clip"),Os={ask:"text","ask-edited":"text","item-message":"text","owner-answer":"note"};async function ri(e,t,i){const n=j(t,i).replace(/\s+/g," ");return(await G(h.join(e,"data","events.jsonl"))).map(s=>String(s.data?.[Os[s.kind]]??"").replace(/\s+/g," ").toLowerCase()).some(s=>s.includes(n.toLowerCase()))||u(`${i} must quote the owner's words in an ask, card message or answer note on this board`),n}r(ri,"ownerQuote");async function Dr(e,t,i){return A(e),i===void 0&&u(`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 ri(e,i,"--asked")}"`}r(Dr,"askedGate");async function Je(e,t,i="--from-ask"){return me(t,i),(await G(h.join(A(e),"data","events.jsonl"))).some(a=>a.id===t&&a.kind==="ask")?t:u(`no ask "${t}" in this site's data/events.jsonl`)}r(Je,"checkAsk");const I=["done","merged","dropped"],Ar=r(async(e,t)=>I.includes((await v(e,t)).item.status),"isFinished"),ci=["ready","grilling","prototyping"],Ps=/^mark (?:it |this |the card )?ready\b/i;function ht(e){return["building","fixing"].includes(e?.status)?(delete e.status,e.status_at=new Date().toISOString(),!0):!1}r(ht,"clearBuildingStatus");function Ds(e,t,i){const n=[].concat(t).map(o=>yn(o)||u(`--pr takes a PR number or owner/repo#<n>, not "${o}"`)),a=r((o,c)=>o.number===c.number&&(o.repo||i||null)===(c.repo||i||null),"same"),s=bn(e);for(const o of n)s.some(c=>a(c,o))||s.push(o);delete e.pr,e.prs=s}r(Ds,"addPRs");const W=r((e,t)=>String(t).includes("/")?String(t):`${e}/${t}`,"depKey");async function ze(e,t,i){const n=(i.deps||[]).map(s=>W(t,s)),a=await Promise.all(n.map(s=>v(e,s).then(({item:o})=>o,()=>null)));return n.filter((s,o)=>a[o]&&!I.includes(a[o].status))}r(ze,"unfinishedDeps");async function As(e,t){const i=[];for(const n of await q(e)){const a=await $(n.file,null);for(const s of Array.isArray(a?.items)?a.items:[])![void 0,"waiting","held"].includes(s.status)||!(s.deps||[]).some(o=>W(n.slug,o)===t)||(await ze(e,n.slug,s)).length||i.push(`${n.slug}/${s.id}`)}return i}r(As,"unblockedBy");async function di(e,t,i,n){const a=new Set(n.match(/[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),s=new Set((i.deps||[]).map(o=>W(C(t).wave,o)));for(const o of await q(e)){const c=await $(o.file,null);for(const d of c?.items||[]){const f=`${o.slug}/${d.id}`;if(f===t||s.has(f)||I.includes(d.status))continue;[d.id,tt(c,d)].flatMap(l=>[l,`${o.slug}/${l}`]).some(l=>a.has(l))&&u(`--next names unfinished card ${f}; add it with --deps`)}}}r(di,"checkNextDeps");function li(e,t){const i="a builder starts from it, so pass";t.size||u(`${e} has no size; ${i} --size small or large with --state ready`),t.plan||u(`${e} has no notes file; ${i} --notes-file <file.md> with --state ready`)}r(li,"checkSizeAndNotes");async function Rs(e,t,i,n){li(t,n);const a=await N.readFile(await Ge(i,n.plan),"utf8"),s=(await ke(e,t)).asked.filter(o=>o.answers.length&&!a.includes(o.id)&&!(o.prototype_gate&&!o.answers.at(-1).note)).map(o=>o.id);s.length&&u(`${t}'s notes leave out the answered question${s.length===1?"":"s"} ${s.join(", ")}; record each answer in ${n.plan}, naming its id, before it is ready`)}r(Rs,"checkNotesComplete");async function Ge(e,t){const i=r(a=>u(`a notes file is a .md file inside ${e}, named relative to it: "${t}" ${a}`),"bad");(typeof t!="string"||!t.trim())&&i("is empty"),(h.isAbsolute(t)||t.split(/[\\/]/).includes(".."))&&i("leaves the folder"),h.extname(t).toLowerCase()!==".md"&&i("is not a .md file");const n=await N.realpath(h.resolve(e,t)).catch(()=>i("does not exist"));return n.startsWith(await N.realpath(e)+h.sep)||i("leaves the folder"),n}r(Ge,"planPath");async function Rr(e,t){const{source:i,wave:n,item:a}=await v(e,t);if(a.plan)return{wave:n,item:a,markdown:await N.readFile(await Ge(i.dir,a.plan),"utf8")};if(typeof a.notes=="string"&&a.notes.trim())return{wave:n,item:a,markdown:a.notes};u(`${t} has no notes`)}r(Rr,"readNotes");function ui({install:e,installSha:t,installLabel:i,clearInstall:n}){if(e===void 0)return(t!==void 0||i!==void 0)&&u("--install-sha and --install-label go with --install <url>"),n?null:void 0;n&&u("pass --install or --clear-install, not both");let a;try{a=new URL(String(e).trim())}catch{a=null}(!a||!["https:","itms-services:"].includes(a.protocol))&&u(`--install takes an https:// or itms-services:// URL, not "${e}"`),a.protocol==="itms-services:"&&!(a.searchParams.get("action")==="download-manifest"&&/^https:\/\//.test(a.searchParams.get("url")||""))&&u("--install: an itms-services:// URL needs ?action=download-manifest&url=https://\u2026/manifest.plist");const s=t===void 0?void 0:/^[0-9a-f]{7,40}$/i.test(t)?t.toLowerCase():u(`--install-sha takes a commit sha of 7 to 40 hex digits, not "${t}"`);return{url:String(e).trim(),...i===void 0?{}:{label:j(i,"--install-label")},built_at:new Date().toISOString(),...s?{sha:s}:{}}}r(ui,"installOf");async function Nr(e,{install:t,installSha:i}){const n=ui({install:t,installSha:i});n||u("main set needs --install <url>");const a=h.join(A(e),"data","main.json");return await y(a,{install:n}),{file:a,lines:["main install set"]}}r(Nr,"setMainInstall");const fi=r((e,t)=>e.length?`Merged PR${e.length>1?"s":""} ${qe(e,t)}.`:"Merged.","mergedLine");function Ns(e,t,i,n,a,s){return n==="merged"?a??(i.status==="merged"?null:fi(bn(i),s)):(a===void 0&&e&&u(`--status ${n} needs --text "<what was done or left>", which becomes ${t}'s last thread line`),a??null)}r(Ns,"closingLine");async function pi(e,t,{ticket:i,status:n,clearStatus:a,next:s,clearNext:o,pr:c,pushed:d,mergeReady:f,ghSince:p,fromAsk:l,state:w,held:m,unheld:g,pickUpAt:b,notesFile:P,deps:D,parent:S,related:_,relate:x,unrelate:O,install:R,installSha:Z,installLabel:ne,clearInstall:F,title:B,size:oe,model:ie,force:ce,text:ae,who:le,ownerStep:de,ownerDone:vt,check:V},ue){n!==void 0&&a&&u("pass --status or --clear-status, not both"),_!==void 0&&(x!==void 0||O!==void 0)&&u("pass --related, or --relate and --unrelate, not both"),w!==void 0&&!ci.includes(w)&&u("--state takes ready, grilling or prototyping"),ce&&!I.includes(n)&&u(`--force goes with --status ${I.join(", ")}`),l!==void 0&&I.includes(n)&&u("--from-ask opens a card; close it in another call"),ae!==void 0&&j(ae,"--text"),de!==void 0&&j(de,"--owner-step");const ye=V===void 0||V===""?V:gi(V);ae!==void 0&&l===void 0&&!I.includes(n)&&u(`--text goes with --from-ask or --status ${I.join(", ")}`),l!==void 0&&ae===void 0&&u(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),s!==void 0&&o&&u("pass --next or --clear-next, not both"),typeof s=="string"&&!s.trim()&&([s,o]=[void 0,!0]);const Pe=Es({held:m,unheld:g,pickUpAt:b}),be=ui({install:R,installSha:Z,installLabel:ne,clearInstall:F});[n,a,s,o,c,d,f,p,l,w,Pe,P,D,S,_,x,O,be,B,oe,ie,i,de,V].every(E=>E===void 0||E===!1)&&u("nothing to set: pass --status, --clear-status, --next, --clear-next, --pr, --pushed, --gh-since, --from-ask, --state, --held, --unheld, --pick-up-at, --notes-file, --deps, --parent, --related, --relate, --unrelate, --install, --clear-install, --title, --size, --model, --ticket or --check");const H=l===void 0?void 0:await Je(e,l);H&&await Tt(e,H);const{source:T,wave:xe,item:k}=await v(e,t);H&&k.from_ask&&k.from_ask!==H&&u(`${t} came from ask "${k.from_ask}", so ask "${H}" can't be its from-ask; to fold it in: item relate-ask ${H} ${t} --summary "<line>"`),H&&w===void 0&&!k.state&&u("--from-ask files a card, which reads New until it has a state: pass --state ready, grilling or prototyping");const Mi=k.status,on=I.includes(k.status);de!==void 0&&on&&u(`${t} is already ${k.status}, so its ticket closed; an owner step goes on the command that finishes the card: land --owner-step, or builder done before the merge`),de!==void 0&&(k.owner_step=j(de,"--owner-step")),(vt||n==="dropped")&&delete k.owner_step,ye!==void 0&&!k.owner_step&&u(`--check goes with an owner step, which ${t} doesn't have: pass --owner-step "<the step>" with it on builder done or land`),ye&&(k.owner_check={check:ye,since:k.owner_check?.check===ye?k.owner_check.since:new Date().toISOString()}),(ye===""||!k.owner_step)&&delete k.owner_check;const rn=await Promise.all((await te(T.dir,k.id)).map(E=>$(E,null))),Wi=Rt(k,rn);if(P!==void 0&&(await Ge(T.dir,P),k.plan=h.normalize(P)),B!==void 0&&(k.title=j(B,"--title")),wi(k,{size:oe,model:ie}),D!==void 0){const E=await $i(e,C(t).wave,D);E.length?k.deps=E:delete k.deps}if(S!==void 0){const E=await ki(e,C(t).wave,t,S);E?k.parent=E:delete k.parent}let St=_===void 0?null:await Is(e,t,k,_);if(x!==void 0&&(k.related=[...new Set([...(k.related||[]).map(E=>W(C(t).wave,E)),...await $t(e,t,x,"--relate")])]),O!==void 0&&(St=await Bs(e,t,k,await $t(e,t,O,"--unrelate"))),Pe!==void 0&>(k,Pe),n==="building"&&(k.held||k.status==="held")&&u(k.pick_up_at?`${t} is held until ${k.pick_up_at}, when the board resumes it; start it early only with item set ${t} --unheld`:`${t} is held; start it only once the owner resumes it, with item set ${t} --unheld`),n==="planned"&&u("--status planned is gone: an unstarted card has no status. Clear it with --clear-status"),n!==void 0&&(k.status=fn.includes(n)?n:u(`--status takes ${fn.join(", ")}`)),(Ls.includes(n)||c!==void 0||d!==void 0||f!==void 0)&&Cs(k),n==="building"){const E=await ze(e,C(t).wave,k);E.length&&u(`${t} is blocked by ${E.join(", ")}; start it once ${E.length===1?"that is":"those are"} done or merged`)}(n==="building"||I.includes(n))&&delete k.implement_at;const Ve=n==="building"&&e?await oi(e,t):null;Ve&&u(`${t} has no take picked: the owner's answer to ${Ve.id} picks none${Ve.note?` ("${js(Ve.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 Ui=I.includes(n)?await si(e,t,`--status ${n}`,ce):async()=>[],cn=I.includes(n)?Ns(e,t,{...k,status:Mi},n,ae,ee(e,xe)):null;if(a&&delete k.status,(n!==void 0||a)&&(k.status_at=new Date().toISOString()),s!==void 0&&(ii(k,s,await ni(e,t)),await di(e,t,k,k.next)),o&&(delete k.next,delete k.next_on_questions),n==="building"&&s===void 0&&k.next_on_questions&&(delete k.next,delete k.next_on_questions),(c!==void 0||d!==void 0||f!==void 0)&&!k.review_since&&(k.review_since=new Date().toISOString()),c!==void 0&&(Ds(k,c,ee(e,xe)),ht(k)),d!==void 0){we().find(E=>ve(E.dir,A(e)))?.remote||u("--pushed is for a board whose repo is a plain git remote; on GitHub, open a PR and pass --pr"),it(k)&&u(`${t} has a PR; its PR says when it merges`);try{ya(d)}catch{u("--pushed takes the name of the branch you pushed")}k.pushed={branch:d,at:new Date().toISOString()},k.branch=d,ht(k)}if(f!==void 0&&(k.merge_ready={branch:f,at:new Date().toISOString()},k.branch=f,ht(k)),I.includes(n)&&delete k.merge_ready,p!==void 0&&((typeof p!="string"||!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(p)||Number.isNaN(Date.parse(p)))&&u("--gh-since takes an ISO-8601 UTC time such as 2026-10-23T11:00:00Z"),k.gh_since=p),H&&(k.from_ask=H),w==="ready"&&e){const E=(await ke(e,t)).asked.filter(jt=>jt.state==="open").map(jt=>jt.id);E.length&&u(`${t} has ${E.length===1?"an open question":"open questions"} ${E.join(", ")}; it is ready once the owner has answered ${E.length===1?"it":"them"}`),await Rs(e,t,T.dir,k)}w!==void 0&&(k.state=w,delete k.unfiled),k.state==="ready"&&Ps.test(k.next??"")&&u(`${t} would read ready with Next "${k.next}", which asks for the state it is already in; pass --next with what happens next, or --clear-next`);const Ye=w==="ready"&&e&&k.implement_at?await Promise.all((await ze(e,C(t).wave,k)).map(E=>X(e,E))):null;Ye?.length&&delete k.implement_at,be&&(k.install=be),be===null&&delete k.install,!Wi&&!["building","fixing"].includes(n)&&Rt(k,rn)&&u(`that leaves ${t} In Progress with nothing building and no Next; pass --next "<what happens next>"`),(i!==void 0||H&&!k.ticket&&!k.linear?.length)&&await Yt(e,t,xe,k,i,ue,T.dir),await y(T.file,xe),St&&await St();const fe=await Ui();(P!==void 0||w==="ready"&&e)&&fe.push(...await Vt(e,t,"Recorded in the card's notes")),n==="building"&&fe.push(...await Vt(e,t,"Acted on: build started")),Ye&&fe.push(Ye.length?(await Ti(e,t,Ye)).line:`released: ${t} starts building now`),H&&fe.push(...(await re(e,t,{text:ae})).lines),cn&&e&&fe.push(...(await re(e,t,{text:cn,who:le})).lines);const dn=k.owner_step&&I.includes(n)?null:pa({status:n,pr:c??d??f}),ln=dn&&await wa(await bi(e),k,dn,{...ue,repo:ee(e,xe)});ln&&fe.push(ln),(c!==void 0||d!==void 0||f!==void 0||qi.includes(n))&&await Fi(T.dir,k.id,I.includes(n)),(c!==void 0||d!==void 0||f!==void 0||qi.includes(n))&&await mt(e,t);const _t=H?await _e(e):{};if(J(_t,H)!==void 0&&(delete _t[H],await y(he(e),{links:_t})),!I.includes(n)||on)return{file:T.file,lines:fe};const un=await As(e,t),Ki=new Date().toISOString();for(const E of un)await Y(U(e,E),{ts:Ki,who:"claude",text:`Unblocked: ${await X(e,t)} is ${n}.`}),await pe({dir:e},"card-unblocked",{key:E});return{file:T.file,lines:[...un.map(E=>`unblocked: ${E}`),...fe]}}r(pi,"changeItem");function wi(e,{size:t,model:i}){for(const[n,a,s]of[["size",t,["small","large"]],["model",i,["opus","sonnet"]]])a!==void 0&&(a===""?delete e[n]:e[n]=s.includes(a)?a:u(`--${n} takes ${s.join(" or ")}, or "" to clear it`))}r(wi,"setBuildChoice");function Es({held:e,unheld:t,pickUpAt:i}){return[e,t,i!==void 0].filter(Boolean).length>1&&u("pass one of --held, --unheld and --pick-up-at"),i!==void 0?i===""?null:{until:mi(i)}:e?{until:null}:t?null:void 0}r(Es,"holdOf");function mi(e){const t=typeof e=="string"&&ua.test(e)?Date.parse(e):NaN;return Number.isNaN(t)&&u('--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()&&u(`--pick-up-at ${e} has passed; pass a later time, or --unheld to resume the card now`),new Date(t).toISOString()}r(mi,"pickUpTime");const Ls=["building","fixing","checking",...I];function Cs(e){delete e.held,delete e.pick_up_at}r(Cs,"clearHold");function gt(e,t){t?e.held=!0:delete e.held,t?.until?e.pick_up_at=t.until:delete e.pick_up_at,!t&&e.status==="held"&&delete e.status,e.held_at=new Date().toISOString()}r(gt,"setHold");async function Er(e,t,i,{announce:n=!0}={}){const{source:a,wave:s,item:o}=await v(e,t);if(n){const c=new Date(i).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});await Y(U(e,t),{ts:new Date().toISOString(),who:"claude",text:`Resumed: its hold until ${c} ended.`}),(await ze(e,C(t).wave,o)).length||await pe({dir:e},"card-unblocked",{key:t,until:i})}gt(o,null),await y(a.file,s)}r(Er,"pickUp");async function hi(e,t,i){const n=String(i?.key||""),{wave:a,id:s}=C(n),{source:o,wave:c}=await Le(e,a),d={text:"Marked done.",who:"love"},f=c.items.find(m=>m.id===s)||u(`wave "${a}" has no item "${s}"`);if(!(t==="card-done"&&!!f.owner_step)&&["merged","dropped"].includes(f.status))return null;if(t==="card-done")return se(e,n,{status:f.status==="merged"?"merged":"done",force:!0,ownerDone:!0,...d});const l=i.hold?typeof i.until=="string"?new Date(i.until).toISOString():null:void 0;gt(f,i.hold?{until:l}:null),await y(o.file,c),i.hold&&(await Fi(o.dir,s,!1),await mt(e,n));const w=l&&new Date(l).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});return re(e,n,{text:i.hold?w?`Held until ${w}.`:"Held.":"Resumed.",who:"love"})}r(hi,"recordOwnerTap");function gi(e){try{return Ba(Ia(e))}catch(t){return u(t.message)}}r(gi,"checkSpec");async function Lr(e,t,i,n){const{item:a}=await v(e,t);return!a.owner_step||a.owner_check?.check!==i?null:se(e,t,{status:a.status==="merged"?"merged":"done",force:!0,ownerDone:!0,text:`Check passed: ${n}. Closed.`})}r(Lr,"passOwnerCheck");async function Cr(e,t,i,n){const{source:a,wave:s,item:o}=await v(e,t);return!o.owner_step||o.owner_check?.check!==i?!1:(o.owner_check.broken=n,await y(a.file,s),await re(e,t,{text:`The check for "${o.owner_step}" can't run (${n}), so it stopped; Claude is asked to fix it. Done still closes the card.`}),await pe({dir:e},"owner-check-broken",{key:t,check:i,error:n}),!0)}r(Cr,"breakOwnerCheck");async function $i(e,t,i){const n=[...new Set(String(i).split(",").map(a=>a.trim()).filter(Boolean))];for(const a of n)await v(e,W(t,a));return n}r($i,"depList");async function ki(e,t,i,n){if(!String(n).trim())return null;const a=W(t,String(n).trim());a===i&&u("--parent: a card can't be its own Parent");for(let s=a;s;){const o=s===a?await v(e,s):await v(e,s).catch(d=>{if(d instanceof Q)return null;throw d});if(!o)break;const{item:c}=o;c.parent&&W(C(s).wave,c.parent)===i&&u(`--parent: ${a} is a child of ${i}, so it can't be its Parent`),s=c.parent?W(C(s).wave,c.parent):null}return a}r(ki,"parentKey");async function $t(e,t,i,n){const a=t.split("/")[0],s=[...new Set(String(i).split(",").map(o=>o.trim()).filter(Boolean).map(o=>W(a,o)))];s.includes(t)&&u(`${n}: a card can't be related to itself`);for(const o of s)await v(e,o);return s}r($t,"relatedKeys");const yi=r((e,t,i)=>async()=>{for(const n of await q(e)){const a=await $(n.file,null);let s=!1;for(const o of Array.isArray(a?.items)?a.items:[]){const c=`${n.slug}/${o.id}`;c===t||!i(c)||!(o.related||[]).some(d=>W(n.slug,d)===t)||(o.related=o.related.filter(d=>W(n.slug,d)!==t),o.related.length||delete o.related,s=!0)}s&&await y(n.file,a)}},"dropLinksTo");async function Is(e,t,i,n){const a=await $t(e,t,n,"--related");return a.length?i.related=a:delete i.related,yi(e,t,s=>!a.includes(s))}r(Is,"setRelated");function Bs(e,t,i,n){const a=(i.related||[]).filter(s=>!n.includes(W(C(t).wave,s)));return a.length?i.related=a:delete i.related,yi(e,t,s=>n.includes(s))}r(Bs,"unrelateCards");const ee=r((e,t)=>e&&we().find(i=>ve(i.dir,e))?.github||t.repo,"boardRepoOf");async function Ir(e,t){const{wave:i,item:n}=await v(e,t);it(n)||u(`${t} has no PR`);const a=Ne(n,ee(e,i));a.length||u(`${t} has no repo`);const s=n.gh_since||"1970-01-01T00:00:00Z";try{const o=ge(await M(e)).github_login||await _a();return await Promise.all(a.map(async({repo:c,number:d})=>({repo:c,...$n(await gn({repo:c,pr:d,since:s}),{pr:d,since:s,me:o})})))}catch(o){u(o.message)}}r(Ir,"prReport");async function Br(e,t,{replyTo:i,body:n}){const a=i===void 0?"pr comment":"pr reply",s=yn(t)||u(`${a} takes a PR number or owner/repo#<n>`);i!==void 0&&!/^[1-9]\d*$/.test(i)&&u("pr reply takes the id of the review comment it answers");const o=j(n,"--body").trim();(o.startsWith(Te.trim())||o.includes(Ct))&&u(`${a} adds the ${Te.trim()} prefix and the claude-bridge marker itself; pass only your words`);const c=s.repo||ee(A(e),{})||u(`${a} needs a board on GitHub; this board has no repo there`);try{const d=await ja({repo:c,pr:s.number,replyTo:i,words:o,login:ge(await M(e)).github_login});return{lines:[`posted on PR ${s.repo?t:`#${t}`}: ${d}`]}}catch(d){u(d.message)}}r(Br,"postPR");const bi=r(async e=>Qn((await M(e)).tickets),"boardSource");async function Tr(e,t,{afterApproval:i,confirmed:n,tapped:a}={}){A(e);const{wave:s,item:o}=await v(e,t);it(o)||u(`${t} has no PR`),I.includes(o.status)&&u(`${t} is ${o.status}`);const c=ee(e,s),d=Ne(o,c);d.length||u(`${t} has no repo`);const f=new Date().toISOString();let p;try{p=await Promise.all(d.map(x=>gn({repo:x.repo,pr:x.number,since:f})))}catch(x){u(x.message)}const l=p.map(({view:x})=>x);if(kn(l)){const x=d.filter((O,R)=>l[R].state==="MERGED");return se(e,t,{status:"merged",text:`PR${x.length>1?"s":""} ${qe(x,c)} ${x.length>1?"were":"was"} already merged.`})}const w=d.map((x,O)=>({...x,...p[O]})).filter(({view:x})=>x.state==="OPEN"),m=Fe(s,(await M(e)).reviewers||[]).length>0,g=[];let b=!1,P=!1;for(const x of w){const{view:O,reviews:R}=x,{failing:Z,pending:ne}=$n(x,{pr:x.number,since:f,me:null}),F=Zi({...O,checks:{state:Z.length?"fail":ne?"pending":"pass",failing:Z,pending:ne}},{needsApproval:m}),B=R.filter(ie=>ie.state==="APPROVED").sort((ie,ce)=>String(ie.submitted_at).localeCompare(String(ce.submitted_at))).at(-1)?.commit_id;let oe=!!(B&&O.headRefOid&&B!==O.headRefOid);oe&&i===void 0&&zs(o,B,O.headRefOid)&&(oe=!1,P=!0),b||=oe,oe&&i===void 0&&F.push('commits after the approval: pass --after-approval "<why they are trivial>" when they are, else ask the approver to look again'),g.push(...d.length>1?F.map(ie=>`${qe([x],c)} ${ie}`):F)}w.length||g.push(d.length>1?"every PR is closed":"PR is closed"),i!==void 0&&!b&&u("--after-approval: no commit came after the approval; drop it");const D=o.manual_steps?.length?n===void 0?null:await ri(e,n,"--confirmed"):void 0;if(n!==void 0&&D===void 0&&u(`--confirmed: ${t} has no manual steps; drop it`),D===null&&g.push(`manual steps: merge only on the owner's word, with --confirmed "<their words>"`),g.length){const x=`Not merged: ${g.join("; ")}.`;a&&await re(e,t,{text:x}),u(x)}const S=[];for(const x of w){const O=await new Promise(R=>{Pt("gh",["pr","merge",String(x.number),"--repo",x.repo,"--squash"],{encoding:"utf8",timeout:12e4},(Z,ne,F)=>R(Z?String(F||Z.message).trim().split(`
|
|
17
|
-
`)[0]:null))});O&&u(`gh pr merge ${qe([x],c)} failed: ${O}${S.length?` (merged ${qe(S,c)} before it)`:""}`),S.push(x)}const _=[fi(S,c),i!==void 0&&`After the approval: ${j(i,"--after-approval")}`,P&&`After the approval: only the board's rebase onto ${o.rebased.at(-1).base}.`,D&&`The owner's word on the manual steps: "${D}"`];return se(e,t,{status:"merged",text:_.filter(Boolean).join(" ")})}r(Tr,"mergeCard");async function qr(e,t){return(await v(e,t)).item.merge_ready||null}r(qr,"heldMerge");async function Fr(e,t){const{source:i,wave:n,item:a}=await v(e,t);return(!a.merge_ready||I.includes(a.status))&&u(`${t} isn't waiting for its Merge`),a.merge_ready.tapped=new Date().toISOString(),await y(i.file,n),a.merge_ready}r(Fr,"tapMerge");async function Mr(e,t){const{source:i,item:n}=await v(e,t);return(await Promise.all((await te(i.dir,n.id)).map(s=>$(s,null)))).filter(s=>s?.worktree?.path).sort((s,o)=>String(o.started).localeCompare(String(s.started)))[0]?.model}r(Mr,"builderModel");async function Yt(e,t,i,n,a,s,o){try{const c=await M(e),d=Et(c.tickets||{source:"internal"}),f=r((m,g)=>m.internal===!0?String(g.id).replace(/^S(\d+)$/,`${d.prefix}-$1`):null,"ownId");a===void 0&&d.source==="internal"&&Lt(d,f(i,n))&&(a=f(i,n));let p=0;if(d.source==="internal")for(const m of await q(e)){const g=await $(m.file,{});for(const b of g.items||[]){const P=[b.ticket,f(g,b)];if(p=Math.max(p,...P.map(D=>Lt(d,D)||0)),a!==void 0&&`${m.slug}/${b.id}`!==t&&P.includes(a))throw Object.assign(new Q(`${a} is already the ticket of ${m.slug}/${b.id}`),{held:!0})}}const l=a===void 0&&d.source!=="internal"&&n.plan?await N.readFile(await Ge(o,n.plan),"utf8"):n.notes||"",w=await fa(d,{ticket:a,title:n.title,body:l,repo:ee(e,i),next:p+1},s);n.ticket=w.ticket,w.created&&d.source!=="internal"?n.ticket_created=!0:delete n.ticket_created}catch(c){if(c instanceof Q)throw c;u(c.message)}}r(Yt,"assignTicket");const je=r((e,t)=>nt(h.join(e||rt(),"tickets"),t,{heartbeat:!0}),"ticketWrite"),se=r((e,t,i,n)=>i.ticket!==void 0||i.fromAsk!==void 0?je(e,()=>pi(e,t,i,n)):pi(e,t,i,n),"setItem"),Wr=r((e,t,i,n)=>je(e,()=>Ts(e,t,i,n)),"addItem");async function Ts(e,t,{title:i,notes:n,deps:a,notesFile:s,ticket:o,pickUpAt:c,parent:d,size:f,model:p,state:l,relate:w,intake:m=!1},g){const{wave:b,id:P}=t.includes("/")?C(t):{wave:t};P!==void 0&&me(P,"the item id"),l!==void 0&&!ci.includes(l)&&u("--state takes ready, grilling or prototyping");const{source:D,wave:S}=await Le(e,b);P!==void 0&&S.items.some(O=>O.id===P)&&u(`wave "${b}" already has an item "${P}"`);const _={id:P,title:j(i,"--title"),created:new Date().toISOString()};d!==void 0&&(_.parent=await ki(e,b,null,d)||u("--parent takes a card, such as <wave>/<id>")),n!==void 0&&(_.notes=j(n,"--notes")),wi(_,{size:f,model:p}),a!==void 0&&(_.deps=await $i(e,b,a)),s!==void 0&&(await Ge(D.dir,s),_.plan=h.normalize(s)),_.deps?.length&&(_.status="waiting"),c!==void 0&>(_,{until:mi(c)}),w!==void 0&&(_.related=await $t(e,t,w,"--relate")),l==="ready"&&li(P===void 0?"the card":t,_),l!==void 0?_.state=l:m&&e&&(_.unfiled=!0),await Yt(e,t,S,_,o,g,D.dir),P===void 0&&(_.id=ha(_.ticket),S.items.some(O=>O.id===_.id)&&u(`wave "${b}" already has an item "${_.id}"; link ${_.ticket} with item add ${b}/<id> --ticket ${_.ticket}`));const x=`${b}/${_.id}`;return S.items.push(_),await y(D.file,S),_.unfiled&&await pe(we().find(O=>ve(O.dir,e))||{dir:e},"card-added",{key:x,title:_.title}),{file:D.file,key:x,lines:[]}}r(Ts,"createItem");const Ur=r((e,{dryRun:t=!1}={},i)=>je(A(e),()=>qs(e,t,i)),"backfillTickets");async function qs(e,t,i){const n=await bi(e)||u("this board has no ticket source yet; pick one in Board settings first"),a=new Set,s=[];for(const p of await q(e)){const l=await $(p.file,null);if(Array.isArray(l?.items)){s.push({entry:p,wave:l});for(const w of l.items)w.ticket&&a.add(w.ticket),l.internal===!0&&n.source==="internal"&&a.add(String(w.id).replace(/^S(\d+)$/,`${n.prefix}-$1`))}}const o=[],c={linked:0,created:0,failed:0},d=r((p,l)=>l.ticket&&!(n.source!=="internal"&&p.internal===!0&&l.ticket===l.id),"ticketed");for(const{entry:p,wave:l}of s)for(const w of l.items){if(d(l,w)||I.includes(w.status))continue;const m=`${p.slug}/${w.id}`,g=[...ma(n,w.title),...(w.linear||[]).filter(b=>Lt(n,b))].find(b=>n.source!=="internal"||!a.has(b));if(t){c[g?"linked":"created"]++,o.push(`${m} would ${g?`link ${g}`:"get a new ticket"}`);continue}try{await Yt(e,m,l,w,g,i,p.dir),await y(p.file,l),a.add(w.ticket),c[g?"linked":"created"]++,o.push(`${m} ${g?"linked":"created"} ${w.ticket}`)}catch(b){if(!(b instanceof Q))throw b;c.failed++,o.push(`${m} failed: ${b.message}`)}}const f=t?`would link ${c.linked}, create ${c.created}`:`linked ${c.linked}, created ${c.created}, failed ${c.failed}`;return{lines:[...o,`backfill${t?" (dry run)":""}: ${f}`]}}r(qs,"backfill");const Kr=r(e=>je(e,async()=>{const t=[];for(const i of await q(e)){const n=await $(i.file,null);if(!Array.isArray(n?.items)||!Array.isArray(n.followups))continue;const a=n.followups.find(d=>n.items.some(f=>f.id===d.id));a&&u(`${i.slug}: follow-up ${a.id} has the id of a card; rename one, then restart the board`);const s=[];for(const{id:d,title:f,from:p,filed:l,done:w,...m}of n.followups){const g=w?e&&(await G(U(e,`${i.slug}/${d}`))).map(b=>b.ts).filter(Boolean).sort().pop()||l:null;s.push({id:d,title:f,...l?{created:l}:{},...p?{parent:W(i.slug,p)}:{},...w?{status:"done"}:{},...g?{status_at:g}:{},...m})}const{followups:o,...c}=n;await y(i.file,{...c,items:[...n.items,...s]}),t.push(`${i.slug}: ${s.length} follow-ups moved to cards (${s.filter(d=>d.status==="done").length} done)`)}return t}),"migrateFollowups"),Hr=r(e=>je(e,async()=>{const t=[];for(const i of await q(e)){const n=await $(i.file,null),a=(Array.isArray(n?.items)?n.items:[]).filter(s=>s.status==="planned");if(a.length){for(const s of a)delete s.status;await y(i.file,n),t.push(`${i.slug}: ${a.length} planned cards now have no status`)}}return t}),"dropPlannedStatus"),kt=r(e=>h.join(e,"data","done-archive.json"),"archiveFile"),Xt=r((e,t)=>nt(h.join(e,"done-archive"),t,{heartbeat:!0}),"archiveWrite"),yt=r(async e=>(await $(kt(e),{cards:[]})).cards||[],"readArchive"),bt=r(e=>`${e.closed_at||""}|${e.key}`,"archiveRank"),Fs=r((e,t)=>bt(e)<bt(t)?1:bt(e)>bt(t)?-1:0,"byClosed"),en=r(e=>h.join(e,"data","done-archive-count.json"),"archiveCountFile");async function Jr(e){const t=(await $(en(e),null))?.count;if(Number.isInteger(t))return t;const i=(await yt(e)).length;return await y(en(e),{count:i}),i}r(Jr,"archiveCount");async function tn(e,t){t.length?await y(kt(e),{cards:t.sort(Fs)}):await N.rm(kt(e),{force:!0}),await y(en(e),{count:t.length})}r(tn,"writeArchive");const zr=r((e,t)=>Xt(e,async()=>{const i=new Map(t.map(l=>[l.key,l])),n=new Date().toISOString(),a=[],s=[];for(const l of await q(e)){const w=await $(l.file,null);if(!Array.isArray(w?.items))continue;const m=w.items.filter(g=>i.has(`${l.slug}/${g.id}`)&&I.includes(g.status));if(m.length){for(const g of m)a.push({...i.get(`${l.slug}/${g.id}`),wave:l.slug,archived_at:n,item:g});s.push([l.file,{...w,items:w.items.filter(g=>!m.includes(g))}])}}if(!a.length)return[];const o=await $(L(e),{items:[]}),c=new Map(a.map(l=>[`item:${l.key}`,l]));for(const l of a)l.questions=(o.items||[]).filter(w=>w.topic===`item:${l.key}`&&w.resolved);const d=await _e(e);for(const l of a)l.asks=l.item.from_ask&&J(d,l.item.from_ask)===void 0?[l.item.from_ask]:[];const f=new Set(a.map(l=>l.key));await tn(e,[...(await yt(e)).filter(l=>!f.has(l.key)),...a.map(({key:l,wave:w,archived_at:m,closed_at:g,item:b,questions:P,asks:D})=>({key:l,wave:w,archived_at:m,closed_at:g,item:b,questions:P,asks:D}))]),a.some(l=>l.asks.length)&&await y(he(e),{links:{...d,...Object.fromEntries(a.flatMap(l=>l.asks.map(w=>[w,l.key])))}});for(const[l,w]of s)await y(l,w);const p=(o.items||[]).filter(l=>!(c.has(l.topic)&&l.resolved));return p.length!==(o.items||[]).length&&await y(L(e),{...o,items:p}),[...f]}),"archiveCards");async function Ms(e,t){return!e||!De(kt(e))?!1:Xt(e,async()=>{const i=await yt(e),n=i.find(l=>l.key===t);if(!n)return!1;const a=(await q(e)).find(l=>l.slug===n.wave),s=a&&await $(a.file,null);if(!Array.isArray(s?.items))return!1;s.items.some(l=>l.id===n.item.id)||await y(a.file,{...s,items:[...s.items,n.item]});const o=await $(L(e),{items:[]}),c=new Set((o.items||[]).map(l=>l.id)),d=(n.questions||[]).filter(l=>!c.has(l.id));d.length&&await y(L(e),{...o,items:[...o.items||[],...d]});const f=await _e(e),p=(n.asks||[]).filter(l=>J(f,l)===t);return p.length&&await y(he(e),{links:Object.fromEntries(Object.entries(f).filter(([l])=>!p.includes(l)))}),await tn(e,i.filter(l=>l!==n)),!0})}r(Ms,"restoreArchived");const xi=["push_tag","no_change"],vi=r(e=>Object.fromEntries(Object.entries(e).filter(([t])=>!xi.includes(t))),"stripQuestion"),Si=r(({summary:e,...t})=>t,"stripItem"),_i=r(e=>xi.some(t=>t in e),"holdsDead"),Gr=r(e=>Xt(e,async()=>{for(const n of await q(e)){const a=await $(n.file,null);Array.isArray(a?.items)&&a.items.some(s=>"summary"in s)&&await y(n.file,{...a,items:a.items.map(Si)})}const t=await $(L(e),null);(t?.items||[]).some(_i)&&await y(L(e),{...t,items:t.items.map(vi)});const i=await yt(e);i.some(n=>"card"in n||"summary"in n.item||(n.questions||[]).some(_i))&&await tn(e,i.map(({card:n,...a})=>({...a,...n?{closed_at:a.closed_at||n.sortAt}:{},item:Si(a.item),questions:(a.questions||[]).map(vi)})))}),"stripDeadFields"),ji=r(e=>`lineup-${e}.md`,"lineupNoteFile"),Ws=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:u(`a wave's slug uses only a-z, 0-9 and "-", not "${e}"`),"lineupSlug");async function xt(e,t){for(const i of await q(e)){const n=await $(i.file,null),a=(n?.lineups||[]).find(s=>s.slug===t);if(a)return{source:i,wave:n,lineup:a}}return u(`this board has no wave "${t}"; lineup create makes one`)}r(xt,"findLineup");async function Us(e,t){(await M(e)).items!=="waves"&&u(`topic wave:${t} is for a board over ~/.claude/waves; a lineup's triage uses lineup:<slug>`),await Le(e,t)}r(Us,"triagedWave");function Qe(e,t,i,n){return i.map(a=>{const{wave:s,id:o}=C(a);s!==e.slug&&u(`${a} is not in wave file "${e.slug}"; a wave's cards share one file`),t.items.some(d=>d.id===o)||u(`wave "${s}" has no item "${o}"`);const c=(t.lineups||[]).find(d=>d.slug!==n&&!d.finished_at&&At(d).flat().includes(o));return c&&u(`${a} is already in the wave "${c.slug}"; lineup remove ${c.slug} ${a} first`),o})}r(Qe,"lineupCards");function Oi(e){const t=[e??[]].flat().map(i=>String(i).split(",").map(n=>n.trim()).filter(Boolean));return(!t.length||t.some(i=>!i.length))&&u("--phase takes one phase's cards, as <wave>/<id>,<wave>/<id>, once per phase in build order"),t}r(Oi,"phaseKeys");function Pi(e,t,i,n){const a=new Set(i.map(s=>`${e.slug}/${s}`));for(const s of i){const o=t.items.find(f=>f.id===s),c=(o.deps||[]).filter(f=>!a.has(W(e.slug,f))),d=n.findIndex(f=>f.includes(s));d>0&&c.push(...n[d-1]),c.length?o.deps=c:delete o.deps}}r(Pi,"chain");async function Di(e,t,i,n,a){a!==void 0&&await ct(h.join(t.dir,ji(n.slug)),`${j(a,"the note").replace(/\n*$/,`
|
|
18
|
-
`)
|
|
14
|
+
${dn}`,login:l});return await ba(e,i,{state:"posted",text:o,review_url:w}),{lines:[`posted review on PR #${t}: ${w}`]}}catch(u){if(u instanceof ie)throw u;d(u.message)}}r(Kr,"postIncomingReview");const Et=r(e=>$.join(e,"data","handshake.json"),"handshakeFile");async function ya(e){const t=await g(Et(e),null);if(!t?.hello)return t;const{hello:n,...a}=t;return{...a,ping:a.ping||n}}r(ya,"readHandshake");async function Jr(e,t){P(e),ge(t,"the event id");const n=(await K($.join(e,"data","events.jsonl"))).find(i=>i.id===t);n?.kind!=="ping"&&n?.kind!=="hello"&&d(`no ping event "${t}" in this site's data/events.jsonl`);const a=await ya(e)||{};return a.ping?.id&&a.ping.id!==t&&d(`"${t}" is an older ping; ack the newest, "${a.ping.id}"`),await y(Et(e),{...a,ping:a.ping||{id:t,at:n.ts,session:null},ack:{id:t,at:new Date().toISOString()}}),Et(e)}r(Jr,"ackPing");async function Gr(e){const t=await ya(e);t&&!t.done_at&&await y(Et(e),{...t,done_at:new Date().toISOString()})}r(Gr,"finishHandshake");const gn={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})$/},ot=r((e,t)=>e.trim().toLowerCase()===t.trim().toLowerCase(),"sameName");function $n(e,t){return Array.isArray(e)||d(`${t} must be a JSON array of reviewers`),e.forEach((n,a)=>{const i=`${t}[${a}]`;Fe(n,Object.keys(gn),i);for(const[o,s]of Object.entries(gn))(typeof n[o]!="string"||!s.test(n[o].trim()))&&d(`${i}.${o} must match ${s}`);e.slice(0,a).some(o=>ot(o.name,n.name))&&d(`${t} names "${n.name}" twice`)}),e.map(n=>Object.fromEntries(Object.keys(gn).map(a=>[a,n[a].trim()])))}r($n,"checkReviewers");async function kn(e,t){const{file:n,reviewers:a}=await rt(e,o=>{const s=$n(t(o.reviewers||[]),"the reviewers");return{set:{reviewers:s},reviewers:s}}),i=[];for(const o of await T(e)){const s=await g(o.file,null);(s?.items||[]).some(c=>!G(c))&&(await y(o.file,{...s,reviewers:a}),i.push(`wave ${o.slug}: reviewers copied`))}return{file:n,lines:i}}r(kn,"writeReviewers");const Qr=r((e,t)=>kn(e,()=>$n(t,"the reviewer file")),"setReviewers"),Ue=r(e=>$.join(P(e),"data","pr-slack.json"),"prSlackFile"),Lt=r(e=>g(Ue(e),{}),"readPRSlack"),st=r(e=>!!(e?.ts||e?.posting_at),"held");async function Ne(e,t){return jt(e,async()=>{const n=JSON.parse(await A.readFile(e,"utf8").catch(()=>"{}")),a=t(n);return await at(e,JSON.stringify(n,null,2)+`
|
|
15
|
+
`),a})}r(Ne,"editJSON");async function It(e,t,n){await Ne(e,i=>{const o=t(i);st(o)&&d("that Slack message has already gone out"),o.posting_at=new Date().toISOString()});let a;try{a=await n()}catch(i){throw await Ne(e,o=>{delete t(o).posting_at}),i}try{await Ne(e,i=>{const o=t(i);o.ts=a,delete o.posting_at})}catch(i){d(`posted, but its record failed, so it won't go again: ${i.message}`)}return a}r(It,"postOnce");const Zr=r((e,t,n,a)=>It(Ue(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"),Vr=r((e,t,n)=>It(Ue(e),a=>a[t.pr].replies.find(i=>i.id===t.id).to[t.reviewer],n),"postReply"),Yr=r((e,t)=>Ne(Ue(e),n=>{const a=n[t.pr].links[t.reviewer];a.reactions.includes(t.emoji)||a.reactions.push(t.emoji)}),"recordReaction"),xa=r((e,t,n,a)=>Ne(Ue(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 Jo(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(Jo,"pushText");async function Xr(e){const t=await g(on,{}),n=r((a,i)=>{const o=Z(t.prs,a)?.head;return!!o&&o!==i.head&&Object.values(i.links).some(s=>s.ts)},"moved");return Object.entries(await Lt(e)).some(([a,i])=>n(a,i))?Ne(Ue(e),a=>{const i=[];for(const[o,s]of Object.entries(a)){if(!n(o,s))continue;const c=Z(t.prs,o),l=Object.keys(s.links).filter(u=>s.links[u].ts);s.head&&(s.replies.push({id:`push-${s.replies.length+1}`,text:Jo(c,o.split("#")[1],s.head),to:Object.fromEntries(l.map(u=>[u,{}]))}),i.push(o)),s.head=c.head}return i}):[]}r(Xr,"queuePushes");function Go(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,fn(t),a].filter(Boolean).join(`
|
|
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
|
+
`)),!(i.includes(c)||!await cs(e,c))&&(await Pn(e,c,"answers"),s.push(c))});return{prototype:i,staged:s}}r(xc,"settleAnswers");async function La(e,t,n,a){try{await a()}catch(i){if(!(i instanceof ie))throw i;await J(e,t,{text:`Your answers were recorded, but ${n} failed: ${i.message}`})}}r(La,"followUp");const Ia=r(e=>typeof e=="string"&&e.startsWith("item:")&&!e.startsWith("item:ask:")?e.slice(5):null,"cardOfTopic"),Ct=r(e=>String(e).padStart(2,"0"),"pad2"),jn=r((e=new Date)=>`${Ct(e.getMonth()+1)}/${Ct(e.getDate())} ${Ct(e.getHours())}:${Ct(e.getMinutes())}`,"noteStamp");function rs(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(rs,"answerLine");async function On(e,t,n,a){const{source:i,wave:o,item:s}=await v(e,t);let c;if(s.plan)c=await Qe(i.dir,s.plan,{missingOk:!0}),me(c)||(await A.writeFile(c,`# ${s.title}
|
|
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*$/,`
|
|
21
|
+
`)}
|
|
22
|
+
## ${n}
|
|
23
|
+
${a}
|
|
24
|
+
`)}r(On,"addToNotes");async function cs(e,t){const{source:n,item:a}=await v(e,t);return Je(a)||Te(a)||a.pushed||(await Promise.all((await z(n.dir,a.id)).map(o=>g(o,null)))).some(o=>o?.started&&!o.done&&o.kind!==le)||(await ue(e,t)).asked.some(o=>o.state==="open")?!1:!await Dn(e,t)}r(cs,"stageable");const ls={answers:"Staging: your answers are in its notes; the orchestrator judges them next.",implement:"Staging: your Implement is in its notes; the orchestrator judges it next.",reply:"Back to staging: your reply is in its notes; the orchestrator judges it before it goes ready again."};async function Pn(e,t,n){const{source:a,wave:i,item:o}=await v(e,t);o.state!=="staging"&&(Object.assign(o,{state:"staging",staged_at:new Date().toISOString()}),delete o.unfiled,await y(a.file,i),await J(e,t,{text:ls[n]}))}r(Pn,"stage");const Ba=r(({text:e,files:t})=>[String(e||"").trim(),...(Array.isArray(t)?t:[]).map(n=>`Attached: ${n?.path||n?.name||n}`)].filter(Boolean).join(`
|
|
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
|
+
`)}`),await y(t.file,n);const{tickets:o}=_e(await te(e));return{file:t.file,lines:[`order: ${pi(n,a.phases,o)}`]}}r(fi,"writeLineup");const pi=r((e,t,n)=>t.map(a=>a.map(i=>St(e,e.items.find(o=>o.id===i)||{id:i},n)).join(", ")).join(" \u2192 "),"orderLine");async function Xc(e,t,{title:n,goal:a,phases:i,note:o}){Bs(t);const s=di(i);o===void 0&&d("lineup create needs the wave note, as --note <text> or --file <path>");const c=await T(e);c.some(f=>f.slug===t)&&d(`"${t}" is a wave file on this board; pick another slug`);for(const f of c)((await g(f.file,null))?.lineups||[]).some(m=>m.slug===t)&&d(`this board already has a wave "${t}"`);const l=c.find(f=>f.slug===N(s[0][0]).wave)||d(`this board has no wave "${N(s[0][0]).wave}"`),{wave:u}=await Me(e,l.slug),w=s.map(f=>ut(l,u,f,t));new Set(w.flat()).size!==w.flat().length&&d("--phase names a card twice");const p={slug:t,title:j(n,"--title"),goal:j(a,"--goal"),phases:w,created:new Date().toISOString()};return ui(l,u,w.flat(),w),u.lineups=[...u.lineups||[],p],fi(e,l,u,p,o)}r(Xc,"createLineup");async function el(e,t,{add:n,after:a,remove:i,phases:o,title:s,goal:c,note:l}){[n,i,o,s,c,l].every(m=>m===void 0)&&d("nothing to change: pass --add, --remove, --phase, --title, --goal, --note or --file"),a!==void 0&&n===void 0&&d("--after goes with --add");const{source:u,wave:w,lineup:p}=await Kt(e,t);p.finished_at&&d(`the wave "${t}" is finished`),p.phases=tn(p),delete p.items;const f=p.phases.flat();if(n!==void 0){const[m]=ut(u,w,[n],t);f.includes(m)&&d(`${n} is already in the wave "${t}"`);const h=a===void 0?p.phases.length:p.phases.findIndex(k=>k.includes(ut(u,w,[a],t)[0]))+1;h===0&&d(`--after ${a} is not in the wave "${t}"`),p.phases.splice(h,0,[m])}if(i!==void 0){const[m]=ut(u,w,[i],t);f.includes(m)||d(`${i} is not in the wave "${t}"`),f.length===1&&d(`${i} is the wave's last card; lineup finish ${t} ends the wave`),p.phases=p.phases.map(h=>h.filter(k=>k!==m)).filter(h=>h.length)}if(o!==void 0){const m=di(o).map(k=>ut(u,w,k,t)),h=m.flat();(h.length!==f.length||new Set(h).size!==h.length||h.some(k=>!f.includes(k)))&&d("--phase lists each of the wave's cards once"),p.phases=m}return s!==void 0&&(p.title=j(s,"--title")),c!==void 0&&(p.goal=j(c,"--goal")),ui(u,w,[...new Set([...f,...p.phases.flat()])],p.phases),fi(e,u,w,p,l)}r(el,"changeLineup");async function tl(e,t){const{source:n,wave:a,lineup:i}=await Kt(e,t);return i.finished_at=new Date().toISOString(),await y(n.file,a),{file:n.file}}r(tl,"finishLineup");async function nl(e,t,n){const{source:a,wave:i,lineup:o}=await Kt(e,t),s=await A.readFile($.join(a.dir,li(t)),"utf8").catch(()=>"");return{title:o.title,markdown:`**Goal** ${o.goal}
|
|
19
28
|
|
|
20
|
-
**Order** ${
|
|
29
|
+
**Order** ${pi(i,tn(o),n)}
|
|
21
30
|
|
|
22
|
-
${o}`}}r(Yr,"readLineupNotes");async function K(e,t,i,n="main"){const{id:a}=C(t);/^[\w-]{1,40}$/.test(n)||u(`--agent takes a name of letters, digits, "_" and "-", not "${n}"`);const s=n==="main"?`${a}.json`:`${a}@${n}.json`;if(i)return h.join(h.resolve(i),"agents",s);const{source:o}=await v(e,t);return h.join(o.dir,"agents",s)}r(K,"builderFile");const Ri=r((e,t)=>typeof e=="string"&&/^\d{4}-\d\d-\d\dT/.test(e)&&!Number.isNaN(Date.parse(e))?new Date(e).toISOString():u(`${t} takes an ISO time such as 2026-10-23T16:30:00-07:00`),"isoTime"),Ks=["reading","building","testing","checking","landing"],Ni=80,Ei="reading, building, testing, checking or landing";function Li(e,t,i,n){e||u(`${n} needs --site, since the line goes to the item's thread`),t!==void 0&&!Ks.includes(t)&&u(`--phase takes ${Ei}, not "${t}"`);const a=j(i,n);return a.length>Ni&&u(`${n} is ${a.length} characters; the cap is ${Ni}. Say it shorter.`),{phase:t,detail:a}}r(Li,"progressLine");const Ci=r((e,t,i)=>t?{detail:e.detail}:(e.phase===void 0&&u(`${i} needs --phase ${Ei}`),e),"forAgent");async function nn(e,t,i="main",n,{phase:a,detail:s},o){const c=Re(n),d=c||i==="main"?"":et(n)||n.kind||i,f=c?Yi[n.kind]:[d,a].filter(Boolean).join(" "),p=new RegExp(`^(?:${f}|${a??f})\\s*:?\\s+`,"i"),l=f?`${f[0].toUpperCase()}${f.slice(1)}: ${s.replace(p,"")}`:s,w=U(e,t);(await G(w)).filter(m=>m.kind==="status").pop()?.text!==l&&await Y(w,{ts:o,who:"claude",kind:"status",...a?{phase:a}:{},...c?{planner:!0}:{},text:l})}r(nn,"statusToThread");const Hs={prototype:"sonnet",grill:"opus"};async function an(e,t,{label:i,phase:n,progress:a,dir:s,agent:o="main",kind:c,model:d,eta:f,worktree:p,ownWorktree:l,dispatch:w}){n!==void 0&&a===void 0&&u('--phase goes with --progress "<detail>"');const m=z(a,T=>Li(e,n,T,"--progress")),g=await K(e,t,s,o),b=new Date().toISOString(),P=z(c,T=>/^[\w.-]{1,30}$/.test(T)?T:u("--kind takes a short word such as claude or codex")),D=Be(P),S=d??(P==="opus"||P==="sonnet"?P:void 0),_=["build",...Ys].includes(D),x=await $(g,null),O=r(T=>["tap","hub"].includes(T?.dispatched)&&!T.done,"tapOpen");if(w==="session"&&O(x))return{file:g,alreadyOpen:!0,lines:[]};o!=="main"&&O(await $(await K(e,t,s),null))&&u(`the Implement tap opened ${t}'s main builder record and no agent has taken it up; run builder start ${t} without --agent first`);const R=x?.dispatched&&!x.done?x:null,Z=_&&!s?(await v(e,t)).item:void 0,ne=x&&Oe(x).length?x:null,F=R&&D==="build"?{kind:Be(R.kind),model:R.model,reason:{tap:"The owner's Build tap picked it.",hub:"The board's pull picked it."}[R.dispatched]??"The session's dispatch picked it."}:_?mn({card:Z??{}}):null;F&&D!=="build"&&D!==F.kind&&u(`--kind ${D}: ${F.kind} builds now. ${F.reason} Pass --kind ${F.kind}, or --kind build.`);const B=F?F.kind:D,oe=wn();if(ia(oe,B))throw new Q(na(oe,B));const ie=m&&Ci(m,Re({kind:B}),"--progress");d!==void 0&&!["opus","sonnet"].includes(d)&&u("--model takes opus or sonnet"),S!==void 0&&B==="codex"&&u("--model is for kind claude; codex needs none");const ce=B==="claude"?ca():"auto";S!==void 0&&ce!=="auto"&&S!==ce&&u(`--model ${S}: the hub's Claude model is ${ce==="opus"?"Opus":"Sonnet"}. Pass --model ${ce}, or no --model.`);const ae=B==="claude"?S??F?.model??(R&&Be(R.kind)==="claude"?R.model:void 0)??"opus":Re({kind:B})?S??(R?.kind===B?R.model:void 0)??Hs[B]:void 0,le={label:i===void 0?"Builder":j(i,"--label"),...B?{kind:B}:{},...ae?{model:ae}:{},started:b,...f===void 0?{}:{eta:Ri(f,"--eta")},...w?{dispatched:w}:{},...ne?{calls:ne.calls,...ne.verified?{verified:ne.verified}:{}}:{},...Qs(x)?{session:x.session}:{}},de=_&&!s&&![...I,"building","fixing"].includes(Z.status),vt=de?await se(e,t,{status:"building"}):{lines:[]};let V,ue=null;const ye=l&&_&&o==="main"?[x?.worktree,Z?.worktree].find(T=>T?.path&&De(T.path))??null:null,Pe=(R??ne)?.worktree??ye,be=p??(_&&o==="main"&&!Pe?l:void 0);try{if(be){const T=await te(h.dirname(h.dirname(g)),C(t).id),xe=(await Promise.all(T.filter(k=>k!==g).map(k=>$(k,null)))).filter(k=>k?.worktree?.path&&!k.done).map(k=>k.worktree.path);({remade:ue,...V}=await be({busy:xe}))}else V=Pe}catch(T){throw de&&await Vs(e,t,Z),T}await y(g,V?{...le,worktree:V}:le),Z?.rebase_run&&o==="main"&&![Ii,"Lander"].includes(le.label)&&await Bi(e,t),F?.inputs&&e&&ba(hn(e),{call:"builder-pick",key:t,result:[B,B==="claude"?ae:null].filter(Boolean).join(" "),reason:F.reason,inputs:{...F.inputs,...S?{askedModel:S}:{},...w?{dispatch:w}:{}}}),ue&&e&&await re(e,t,{text:ue}),ie&&await nn(e,t,o,le,ie,b);const H=await Vt(e,t,"Acted on: builder started");return e&&_&&await _s(e,t),{file:g,kind:B??null,model:ae??null,reason:F?.reason??null,worktree:V?.path??null,lines:[...D==="build"?[`kind: ${B}. ${F.reason}`]:[],...et(le)?[`model: ${et(le)}`]:[],...e?[`ref: ${await X(e,t)}`]:[],...V?[`worktree: ${V.path}`]:[],...ue?[ue]:[],...vt.lines,...H]}}r(an,"startBuilder");async function Js(e,t,i,n="tap"){try{const{held:a,blockers:s}=await Gs(e,t);if(a&&await hi(e,"card-hold",{key:t,hold:!1}),s.length)return await Ti(e,t,s);try{const o=await an(e,t,{kind:"build",worktree:i,dispatch:n});return{key:t,ref:await X(e,t),kind:o.kind,...o.model?{model:o.model}:{},reason:o.reason,worktree:o.worktree}}catch(o){throw a&&await Zs(e,t,a),o}}catch(a){if(!(a instanceof Q))throw a;return await Y(U(e,t),{ts:new Date().toISOString(),who:"claude",kind:Dt,text:`Couldn't start implementing: ${a.message}`}),null}}r(Js,"startTappedBuild");async function Xr(e,t,{pr:i,conflict:n,base:a},s){const{source:o,wave:c,item:d}=await v(e,t);d.rebase_run={pr:i,base:a,conflict:n,at:new Date().toISOString()},await y(o.file,c);try{const f=await an(e,t,{kind:"build",label:Ii,dispatch:"hub",ownWorktree:s});return{key:t,kind:f.kind,...f.model?{model:f.model}:{},worktree:f.worktree}}catch(f){throw await Bi(e,t),f}}r(Xr,"startRebase");const Ii="Rebase";async function Bi(e,t){const{source:i,wave:n,item:a}=await v(e,t);delete a.rebase_run,await y(i.file,n)}r(Bi,"endRebaseRun");async function ec(e,t){const{wave:i,item:n}=await v(e,t),a=ee(e,i);return Ne(n,a).find(s=>s.repo===a)?.number??null}r(ec,"cardPR");async function tc(e,t,{from:i,to:n,replayed:a}){const{source:s,wave:o,item:c}=await v(e,t);return c.rebase_run?(c.rebased=[...c.rebased||[],{from:i,to:n,base:c.rebase_run.base,replayed:a,at:new Date().toISOString()}],delete c.rebase_run,await y(s.file,o),!0):!1}r(tc,"recordRebase");const zs=r((e,t,i)=>{const n=new Map((e.rebased||[]).filter(a=>a.replayed).map(a=>[a.to,a.from]));for(let a=i,s=0;a&&s<=n.size;a=n.get(a),s++)if(a===t)return!0;return!1},"onlyRebased");async function Gs(e,t){const i=wn();i.state==="paused"&&u(ta(i));const{item:n}=await v(e,t),a=n.held||n.status==="held"?{held:n.held,status:n.status,pick_up_at:n.pick_up_at,held_at:n.held_at}:null;a&&![...Tn,"held"].includes(n.status)&&u("it's held. Unhold it, then tap Implement."),await oi(e,t)&&u("no take is picked on its prototypes, so it prototypes again. Pick a take from the next round, then tap Implement.");const s=await Promise.all((await ze(e,C(t).wave,n)).map(o=>X(e,o)));return{held:a,blockers:s}}r(Gs,"tapRefusals");async function nc(e,t){const{item:i}=await v(e,t);if(i.implement_at)return;(i.held||i.status==="held")&&await hi(e,"card-hold",{key:t,hold:!1});const{source:n,wave:a,item:s}=await v(e,t);s.implement_at=new Date().toISOString(),await y(n.file,a)}r(nc,"askImplement");async function Ti(e,t,i){const n=i.join(", ");return await pe({dir:e},"card-bump",{key:t}),await Y(U(e,t),{ts:new Date().toISOString(),who:"claude",text:`Can\u2019t start yet: waits on ${n}. Bumped to the top of Todo.`}),{key:t,ref:await X(e,t),bumped:i,line:`bumped: ${t} waits on ${n}, so it stays in Todo, bumped to the top`}}r(Ti,"bumpBlocked");async function ic(e,t,i){let n;try{n=await Js(e,t,i)}catch(a){await Y(U(e,t),{ts:new Date().toISOString(),who:"claude",kind:Dt,text:`Couldn't start implementing: ${a.message}`}),n=null}if(!n){const{source:a,wave:s,item:o}=await v(e,t);delete o.implement_at,await y(a.file,s)}return n}r(ic,"startReleasedBuild");async function ac(e,t,i){const n=await K(e,t),a=await $(n,null),s=new Date().toISOString();a&&!a.done&&await y(n,{...a,done:s});const{source:o,wave:c,item:d}=await v(e,t);d.status==="building"&&(delete d.status,d.status_at=s),await y(o.file,c),await Y(U(e,t),{ts:s,who:"claude",kind:Dt,text:`Couldn't start implementing: ${i}`})}r(ac,"abandonLaunch");async function sc(e,t,i){const n=await K(e,t),a=await $(n,null);a&&!a.done&&await y(n,{...a,done:new Date().toISOString()}),await mt(e,t),await re(e,t,{text:i})}r(sc,"abandonPlanner");async function oc(e,t,i,n){const a=await K(e,t),s=await $(a,null);await y(a,{...s,session:{id:i,pid:n,at:new Date().toISOString()}})}r(oc,"recordSession");async function rc(e,t){const{source:i}=await v(e,t);return(await Promise.all((await te(i.dir,C(t).id)).map(a=>$(a,null)))).filter(a=>a&&a.started&&!a.done).map(a=>({pid:a.session?.pid??null,at:a.session?.at??null}))}r(rc,"openAgents");async function cc(e,t){return(await G(U(e,t))).filter(i=>i.kind==="status").pop()?.phase==="landing"}r(cc,"isLanding");const Qs=r(e=>!!e?.session&&e.session.at>(e.done??e.started),"launchedSession");async function dc(e,t,i){const n=await $(await K(e,t),null);if(n&&!n.done)return null;const a=await an(e,t,{kind:i,dispatch:"hub"});return{key:t,kind:i,model:a.model}}r(dc,"startPlanner");async function lc(e){const t=[];for(const i of await q(e)){const n=await $(i.file,null);for(const a of n?.items||[]){if(a.status!=="building")continue;const s=await $(h.join(i.dir,"agents",`${a.id}.json`),null);!s?.started||s.done||!s.dispatched||Re(s)||t.push({key:`${i.slug}/${a.id}`,kind:Be(s.kind),model:s.model})}}return t}r(lc,"cutOffBuilds");async function Zs(e,t,i){const{source:n,wave:a,item:s}=await v(e,t);for(const o of Object.keys(i))i[o]===void 0?delete s[o]:s[o]=i[o];await y(n.file,a)}r(Zs,"restoreHold");async function Vs(e,t,i){const{source:n,wave:a,item:s}=await v(e,t);for(const o of["status","status_at"])i[o]===void 0?delete s[o]:s[o]=i[o];await y(n.file,a)}r(Vs,"restoreStatus");async function uc(e,t){return e||u("audit reads a board: pass --site <route>"),t!==void 0&&C(t),{lines:xa(va(hn(e),{key:t}))}}r(uc,"auditLog");const Ys=["codex","claude"];async function fc(e,t){const{item:i}=await v(e,t),{kind:n,model:a,reason:s}=mn({card:i});return{lines:[`${[n,a].filter(Boolean).join(" ")}: ${s}`]}}r(fc,"pickBuilder");async function pc(e,t,i){const{source:n,wave:a,item:s}=await v(e,t);if(s.worktree?.path&&De(s.worktree.path))return null;const o=await sn(e,t);if(o?.path&&De(o.path))return null;const{remade:c,...d}=await i({busy:[]});return s.worktree=d,await y(n.file,a),d}r(pc,"ensureWorktree");async function sn(e,t,{dir:i}={}){const{id:n}=C(t),a=h.dirname(await K(e,t,i)),s=await te(h.dirname(a),n);return(await Promise.all(s.map(c=>$(c,null)))).filter(c=>c?.worktree?.path).sort((c,d)=>String(d.started).localeCompare(String(c.started)))[0]?.worktree??null}r(sn,"recordedWorktree");async function wc(e,t,i,{phase:n,dir:a,agent:s="main",eta:o,clearEta:c}){o!==void 0&&c&&u("pass --eta or --clear-eta, not both");const d=Li(e,n,i,"builder progress"),f=await K(e,t,a,s),p=await $(f,null);(!p?.started||p.done)&&u(`builder progress needs an open builder record on ${t}; run builder start first`);const l=Ci(d,Re(p),"builder progress");return o!==void 0&&(p.eta=Ri(o,"--eta")),c&&delete p.eta,(o!==void 0||c)&&await y(f,p),await nn(e,t,s,p,l,new Date().toISOString()),f}r(wc,"builderProgress");async function mc(e,t){await v(e,t);const i=U(e,t);(await G(i)).filter(n=>n.kind==="status").pop()?.phase!=="landing"&&await Y(i,{ts:new Date().toISOString(),who:"claude",kind:"status",phase:"landing",text:"Landing: rebasing onto main and landing"})}r(mc,"landingLine");const Oe=r(e=>(e?.calls||[]).filter(t=>!e.verified||t.at>e.verified.at),"unverifiedCalls");async function hc(e,t,i,{dir:n,agent:a="main"}){const s=j(i,"builder call");/\n/.test(s)&&u("builder call takes one line; record each call on its own");const o=await K(e,t,n,a),c=await $(o,null);return(!c?.started||c.done)&&u(`builder call needs an open builder record on ${t}; run builder start first`),await y(o,{...c,calls:[...c.calls||[],{at:new Date().toISOString(),text:s}]}),o}r(hc,"recordCall");async function Ze(e,t){const i=await G(U(e,t)),n=new Map(i.filter(a=>a.kind===Xe.answer).map(a=>[a.ask,a]));return i.filter(a=>a.kind===Xe.ask).map(a=>{const s=n.get(a.ask);return{id:a.ask,agent:a.agent||"main",text:a.text,at:a.ts,answer:s?{text:s.text,at:s.ts}:null,deferred:!!s?.deferred}})}r(Ze,"builderAsks");const Xs=r(e=>we().find(t=>ve(t.dir,e))??{dir:e},"boardTarget");async function gc(e,t,i,{agent:n="main"}){A(e);const a=j(i,"builder ask");/\n/.test(a)&&u("builder ask takes one line; ask each question on its own");const s=await $(await K(e,t,void 0,n),null);(!s?.started||s.done)&&u(`builder ask needs an open builder record on ${t}; run builder start first`);const o=`ask${(await Ze(e,t)).length+1}`;return await Y(U(e,t),{ts:new Date().toISOString(),who:`${et(s)||s.kind||"Claude"} builder`,kind:Xe.ask,ask:o,agent:n,text:a}),await pe(Xs(e),"builder-ask",{key:t,ask:o,agent:n,text:a}),{id:o,lines:[]}}r(gc,"askOrchestrator");async function $c(e,t,i,n,{deferred:a=!1}={}){A(e);const s=j(n,"--text"),o=(await Ze(e,t)).find(d=>d.id===i)||u(`no ask "${i}" on ${t}; builder asks ${t} lists them`);o.answer&&u(`${i} on ${t} is already ${o.deferred?"deferred":"answered"}`);const c=new Date().toISOString();if(await Y(U(e,t),{ts:c,who:"claude",kind:Xe.answer,ask:i,...a?{deferred:!0}:{},text:s}),a){const d=await K(e,t,void 0,o.agent),f=await $(d,null);f&&await y(d,{...f,calls:[...f.calls||[],{at:c,text:`Question: ${o.text} (deferred to the owner)`}]})}return{lines:[]}}r($c,"answerBuilder");async function kc(e,t){A(e);const i=await Ze(e,t);return i.length?{lines:i.flatMap(n=>[`${n.id}. ${n.text}`,` ${n.answer?`${n.deferred?"deferred":"answered"}: ${n.answer.text}`:"waiting on the orchestrator"}`])}:{lines:["no asks yet"]}}r(kc,"listAsks");async function eo(e,t,i){const n=t.topic.startsWith("item:")&&!t.topic.startsWith("item:ask:")?t.topic.slice(5):u(`${i}.ask goes on a card's question, topic item:<wave>/<id>`),a=(await Ze(e,n)).find(s=>s.id===t.ask)||u(`${i}.ask names no ask "${t.ask}" on ${n}; builder asks ${n} lists them`);a.deferred||u(`${i}.ask names ${a.id}, which the orchestrator ${a.answer?"answered":"has not deferred"}; only a deferred ask goes to the owner`)}r(eo,"checkAskQuestion");async function yc(e,t,{dir:i,agent:n="main"}){const a=await $(await K(e,t,i,n),null),s=a?.calls||[];if(!s.length)return{lines:["no judgment calls recorded"]};const{verified:o}=a,c=Oe(a).length?"not verified yet":`verified ${o.at}${o.note?`: ${o.note}`:""}`;return{lines:[...s.map((d,f)=>`${f+1}. ${d.text}`),c]}}r(yc,"listCalls");async function bc(e,t,{dir:i,agent:n="main",note:a}){const s=await K(e,t,i,n),o=await $(s,null);if(Oe(o).length||u(`${t} has no unverified judgment calls${n==="main"?"":` from agent ${n}`}; builder calls ${t} lists them`),e){const c=new Set((await $(L(e),{items:[]})).items?.filter(f=>f.topic===`item:${t}`&&f.ask).map(f=>f.ask)),d=(await Ze(e,t)).find(f=>f.deferred&&f.agent===n&&!c.has(f.id));d&&u(`${d.id} on ${t} was deferred to the owner; add its question with "ask": "${d.id}" first`)}return await y(s,{...o,verified:{at:new Date().toISOString(),...a===void 0?{}:{note:j(a,"--note")}}}),s}r(bc,"verifyCalls");async function xc(e,t){const{source:i,item:n}=await v(e,t),a=r((o,c)=>[(o.options||[]).find(d=>d.id===c.choice)?.label,c.note].filter(Boolean).join(": ")||"answered","said"),s=(await ke(e,t)).asked.filter(o=>o.ask&&o.answers.length).map(o=>({ask:o.ask,question:o.question,answer:a(o,o.answers.at(-1))}));return{key:t,ref:await X(e,t),title:n.title||t,notes:n.plan?h.join(i.dir,n.plan):null,worktree:(await sn(e,t))?.path??null,answers:s,links:await to(e,t),rebasePR:n.rebase_run?.pr??null}}r(xc,"briefFacts");async function to(e,t){const i=new Map;for(const c of await q(e))for(const d of(await $(c.file,null))?.items||[])i.set(`${c.slug}/${d.id}`,{entry:c,it:d});const n=C(t).wave,{it:a}=i.get(t),s=[...i].filter(([,c])=>(c.it.related||[]).some(d=>W(c.entry.slug,d)===t)).map(([c])=>c),o=[...a.parent?[["parent",W(n,a.parent)]]:[],...(a.deps||[]).map(c=>["depends on",W(n,c)]),...[...new Set([...(a.related||[]).map(c=>W(n,c)),...s])].map(c=>["related",c])].filter(([,c])=>i.has(c));return Promise.all(o.map(async([c,d])=>{const{entry:f,it:p}=i.get(d);return{relation:c,ref:await X(e,d),title:p.title||d,status:p.status||"not started",notes:p.plan?h.join(f.dir,p.plan):null}}))}r(to,"cardLinks");async function vc(e){const t=[];for(const i of await q(e)){const n=await $(i.file,null);for(const a of n?.items||[]){if(a.status!=="checking")continue;const s=(await Promise.all((await te(i.dir,a.id)).map(f=>$(f,null)))).filter(Boolean),o=s.map(f=>f.verified?.at).filter(Boolean).sort().at(-1);if(!o||s.some(f=>Oe(f).length))continue;const c=`${i.slug}/${a.id}`;if((await ke(e,c)).asked.some(f=>f.state==="open"))continue;const d=s.filter(f=>f.worktree?.path).sort((f,p)=>String(p.started).localeCompare(String(f.started)))[0];d&&De(d.worktree.path)&&t.push({key:c,verified:o,kind:Be(d.kind),model:d.model})}}return t}r(vc,"landable");async function Sc(e,t){const{item:i}=await v(e,t);i.status!=="building"&&u(`${t} is ${i.status||"not started"}; only a card at building has a stopped lander to relaunch`),await se(e,t,{status:"checking",next:"A lander relaunches to land the verified build"});const n=h.join(e,"data","landers.json"),a=await $(n,{});delete a[t],await N.writeFile(n+".tmp",JSON.stringify(a)+`
|
|
23
|
-
`),await
|
|
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 A.rename(o+".tmp",o)}r(Tl,"relaunchLander");async function ql(e,t,{dir:n}={}){const{id:a}=N(t),i=await z($.dirname($.dirname(await C(e,t,n))),a),o=[];for(const s of i){if(!Ee(await g(s,null)).length)continue;const c=$.basename(s,".json").slice(String(a).length+1)||"main";o.push(c==="main"?"":` --agent ${c}`)}o.length&&d(`land: ${t} has judgment calls nobody verified yet; check them with ${o.map(s=>`builder calls ${t}${s}`).join(", ")}, then run ${o.map(s=>`builder verified ${t}${s}`).join(", ")}`)}r(ql,"checkCallsVerified");async function Fl(e,t,{dir:n,agent:a="main",next:i}){const o=await g(await C(e,t,n,a),null),s=o&&!o.done?Ee(o):[];if(!s.length)return null;i!==void 0&&d(`builder done sets ${t}'s Next itself while its record has judgment calls; run it without --next`);const c=s.some(u=>u.text.startsWith("Question:"))?"Answer the builder's questions":"Verify the builder's judgment calls",l=await fe(e,t,{handBack:!0,next:c});return{lines:[`handed back: Next "${c}"`,...l.lines]}}r(Fl,"handBackCalls");async function Ml(e,t,{agent:n="main"}){if(n!=="main")return!1;const a=await C(e,t,void 0,n),i=await g(a,null);if(!i?.started||i.done||!i.worktree?.path)return!1;const{id:o}=N(t);for(const s of await z($.dirname($.dirname(a)),o)){const c=s===a?null:await g(s,null);if(!c?.started||c.done||c.kind==="install")continue;const l=$.basename(s,".json").slice(String(o).length+1);d(`builder done would finish ${t} while agent ${l} is still open; run builder done ${t} --agent ${l} first`)}return!0}r(Ml,"finishesCard");async function Wl(e,t){const{source:n,item:a}=await v(e,t);if(G(a))return null;const i=(await Promise.all((await z(n.dir,a.id)).map(s=>g(s,null)))).filter(Boolean);if(!en(i)||i.some(s=>Ee(s).length||s.started&&!s.done&&s.kind!=="install"))return null;const o=await xi(e,t);return!o||!me(o.path)?null:{waits:(await ue(e,t)).asked.filter(s=>s.state==="open").map(s=>s.id)}}r(Wl,"verifiedFinish");async function Ul(e,t){const{item:n}=await v(e,t);if(n.reopened_at&&d(`the owner reopened ${await X(e,t)}; it closes on the owner's Done`),n.lane!=="done"||Ot(n).some(o=>o.merged_at))return;const a=String(n.closed_at||"");(await K($.join(e,"data","events.jsonl"))).some(o=>o.kind==="item-message"&&o.data?.key===t&&String(o.ts)>a)&&d(`the owner reopened ${await X(e,t)} with a message; it closes on the owner's Done`)}r(Ul,"checkNotReopened");async function z(e,t){const n=$.join(e,"agents");return(await A.readdir(n).catch(()=>[])).filter(a=>a===`${t}.json`||a.startsWith(`${t}@`)&&a.endsWith(".json")).map(a=>$.join(n,a))}r(z,"builderFiles");async function qn(e,t,n,{handedBack:a,endHandBack:i=!1,held:o=!1}={}){const s=new Date().toISOString();for(const c of await z(e,t)){const l=await g(c,null);if(!l)continue;if(l.done||l.kind==="install"&&!n){if(i&&l.handed_back){const{handed_back:f,...m}=l;await y(c,m)}continue}const{eta:u,handed_back:w,...p}=l;await y(c,{...p,done:s,...a?{handed_back:a}:{},...o&&l.started?{hold_stopped:!0}:{}})}}r(qn,"closeBuilders");async function Hl(e,t){const n=await C(e,t),a=await g(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(Hl,"takeHoldStopped");async function Vs(e,t,{dir:n,agent:a,startedBetween:i,note:o,next:s,requireNext:c=!1}){const l=await C(e,t,n,a),u=await g(l,null);if(!u||u.done)return l;if(i){const S=Date.parse(u.started);if(!(S>=i[0]&&S<=i[1]))return l}const{eta:w,...p}=u,f=new Date().toISOString(),m={...p,done:f,...o?{note:o}:{}},{id:h}=N(t),k=$.dirname($.dirname(l)),x=await Promise.all((await z(k,h)).map(S=>S===l?m:g(S,null))),E=!x.some(S=>S?.started&&!S.done),O=n?{file:$.join(k,"wave.json")}:(await v(e,t)).source,B=await g(O.file,null),_=B?.items?.find(S=>S.id===h);return s!==void 0&&(Ma(_||d(`${t} has no item to set --next on`),s,await Fa(e,t)),e&&await za(e,t,_,_.next)),c&&_&&nn(_,x)&&d(`builder done closes ${t}'s last open builder, which leaves it In Progress with no Next; pass --next "<what happens next>", or record a judgment call with builder call ${t} or land`),await y(l,m),await Bt(e,t),o&&await Bn(e,t,a,u,{detail:o},f),s!==void 0&&await y(O.file,B),l}r(Vs,"stopBuilder");async function Ys(e){const t=[];for(const n of await T(e)){const a=await g(n.file,null);for(const i of a?.items||[])for(const o of await z(n.dir,i.id)){const s=await g(o,null);if(!s?.started||s.done)continue;const c=$.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(Ys,"openBoardBuilders");async function zl(e){const t=await Ys(e);for(const n of t)await Vs(e,n.key,{agent:n.agent,note:"Board closed. Builder record stopped; work is kept for reopening."});return t}r(zl,"closeBoardBuilders");export{ie as BoardError,la as PUSH_LINE_MAX,mn as UPLOAD_LIMIT,Nr as WORKING_KINDS,rl as abandonLaunch,cl as abandonPlanner,Jr as ackPing,zc as addItem,gc as addPrototype,yc as addQuestions,ic as addReviewers,fc as addRuleTo,hc as addTopic,_l as addVisual,Nl as answerBuilder,J as appendThread,Vc as archiveCards,Zc as archiveCount,Wt as archiveFile,Ht as archiveRank,Rl as askOrchestrator,Rc as askedGate,jl as auditLog,Kc as backfillTickets,qt as blockWords,te as boardConfig,ne as boardRepoOf,Tc as breakOwnerCheck,Bl as briefFacts,Hc as builderModel,Pl as builderProgress,ml as bumpCard,nc as cardLinked,X as cardName,il as cardPR,el as changeLineup,ql as checkCallsVerified,Dc as checkClose,Ul as checkNotReopened,zl as closeBoardBuilders,Xc as createLineup,Sl as cutOffBuilds,pc as editRuleIn,qr as eventCard,lc as fallBackFromLinear,Gr as finishHandshake,tl as finishLineup,Ml as finishesCard,Fl as handBackCalls,Wc as heldMerge,yl as helperOf,ua as hiddenFile,sl as implementAsIs,Fr as intakeHandoff,Ec as isFinished,vl as isHelper,hl as isLanding,T as itemSources,wl as landFirst,Cl as landable,Al as landingLine,li as lineupNoteFile,El as listAsks,Ll as listCalls,$c as listPrototypes,uc as listRules,Ur as markAsk,Br as markRead,fl as markStopped,Er as markWorking,Mc as mergeCard,Ja as mergedLine,Jc as migrateFollowups,Gc as migrateStatuses,Hr as moveAsk,Nc as moveCard,Oc as moveQuestions,dl as openAgents,Ys as openBoardBuilders,ul as openRuns,cc as opensPRs,N as parseKey,Cc as passOwnerCheck,Ol as pickBuilder,Bc as pickUp,Sc as planAgain,ac as postDigest,Kr as postIncomingReview,Zr as postLink,Fc as postPR,Vr as postReply,qc as prReport,To as pushLine,Mo as pushUrl,Xr as queuePushes,$s as quickAsk,dt as readArchive,Re as readAskLinks,Nt as readAskProgress,yn as readBoardRules,Vo as readBoardSettings,ya as readHandshake,Uo as readHidden,g as readJSON,nl as readLineupNotes,Ho as readMarks,pa as readMarksFile,Lc as readNotes,Lt as readPRSlack,ct as readPrototypes,Tr as readUpload,Ro as readWorking,Dl as recordCall,Va as recordOwnerTap,Eo as recordPush,Yr as recordReaction,ol as recordRebase,zr as recordReview,ll as recordSession,xi as recordedWorktree,Mr as relateAsk,Tl as relaunchLander,bc as removePrototype,oc as removeReviewers,wc as removeRuleFrom,No as repliesSincePush,kl as requestHelper,Pc as resolveNoteAnswers,jc as resolveQuestion,Qo as reviewDigest,ec as reviewDue,tc as reviewUpdated,mc as saveBoardRules,He as saveBoardSettings,sc as saveBoardShare,Cr as saveUpload,Lr as sendPush,kc as servedRoundsOpen,rc as setBoardPause,dc as setBoardSetting,Ir as setHidden,fe as setItem,Ic as setMainInstall,Qr as setReviewers,Yo as settingsLines,_e as settingsOf,xc as settleAnswers,Dr as siteDir,Po as sitesDir,vc as stageImplement,Qc as stageImplementAsks,_c as stageReply,ft as startBuilder,bl as startHelper,gl as startPlanner,al as startRebase,Fs as startTappedBuild,$l as startVisual,Vs as stopBuilder,pl as stopHeldMerge,Yc as stripDeadFields,Sn as takeFile,Hl as takeHoldStopped,Uc as tapMerge,U as threadFile,Ta as topicQuestions,Wr as unfoldAsk,Ac as unrecorded,Wl as verifiedFinish,Il as verifyCalls,xl as waitHelper,At as wavesDir,Co as withReplies,Rr as writeReply};
|