@lovek4d/kickflip 0.1.0 → 0.4.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 +239 -104
- package/README.md +54 -4
- package/SKILL.md +63 -42
- package/bin/doctor.mjs +4 -3
- package/bin/install.mjs +3 -3
- package/bin/kickflip.mjs +103 -40
- package/bin/statusline.mjs +4 -3
- package/bin/wait-events.mjs +2 -2
- package/board/arch-review.mjs +5 -0
- package/board/auto-review.mjs +2 -0
- package/board/bin/agent.mjs +1 -1
- package/board/bin/board.mjs +371 -82
- package/board/bin/builder-stop-hook.mjs +4 -3
- package/board/domain.mjs +8 -3
- package/board/finish.mjs +3 -0
- package/board/headless.mjs +3 -0
- package/board/on-host.mjs +1 -0
- package/board/permissions.mjs +2 -0
- package/board/public/app-arch.js +75 -0
- package/board/public/app-asks.js +66 -28
- package/board/public/app-banner.js +2 -2
- package/board/public/app-cards.js +354 -77
- package/board/public/app-core.js +40 -8
- package/board/public/app-header.js +33 -8
- package/board/public/app-lanes.js +238 -34
- package/board/public/app-needs.js +2 -0
- package/board/public/app-snapshot.js +45 -11
- package/board/public/app-threads.js +20 -21
- package/board/public/app.html +6 -2
- package/board/public/app.js +114 -33
- package/board/public/attach.js +39 -3
- package/board/public/dialogs.js +161 -118
- package/board/public/hub-kit.js +27 -7
- package/board/public/hub.css +26 -33
- package/board/public/linear-key.js +143 -0
- package/board/public/log.html +48 -0
- package/board/public/prompts.js +9 -8
- package/board/public/prototype-kit.js +6 -6
- package/board/public/prototype-switcher.js +13 -5
- package/board/public/prototype.html +1 -0
- package/board/public/router.js +14 -10
- package/board/public/rules-editor.js +4 -5
- package/board/public/shell.js +21 -7
- package/board/public/sidebar.css +51 -11
- package/board/public/sidebar.js +124 -19
- package/board/public/style.css +244 -56
- package/board/public/toasts.js +5 -3
- package/board/public/ui-cards.js +236 -66
- package/board/public/ui-enums.js +5 -4
- package/board/public/ui-header.js +23 -14
- package/board/public/ui-hub.js +34 -16
- package/board/public/ui-links.js +18 -10
- package/board/public/ui-markdown.js +1 -1
- package/board/public/ui-order.js +21 -0
- package/board/public/ui-page-header.js +13 -8
- package/board/public/ui-patch.js +53 -0
- package/board/public/ui-phase-bars.js +17 -0
- package/board/public/ui-prompts.js +8 -8
- package/board/public/ui-questions.js +35 -11
- package/board/public/ui-sidebar.js +83 -21
- package/board/public/ui-snapshot.js +30 -13
- package/board/public/ui-threads.js +38 -23
- package/board/public/ui.js +96 -16
- package/board/public/view-frame.js +1 -1
- package/board/public/view-hub-settings.js +67 -44
- package/board/public/view-hub.js +105 -13
- package/board/public/view-notes.js +8 -5
- package/board/public/view-questions.js +123 -52
- package/board/public/view-settings.js +235 -62
- package/board/review.mjs +1 -0
- package/board/server.mjs +18 -7
- package/board/store.mjs +15 -6
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +20 -13
- package/docs/hub-session.md +4 -4
- package/hooks-plugin/hooks/codex-share-auto.mjs +7 -5
- package/hooks-plugin/hooks/watcher-stop.mjs +41 -0
- package/hub/awake.mjs +1 -0
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/close-board.mjs +1 -1
- package/hub/duration.mjs +1 -0
- package/hub/github.mjs +3 -4
- package/hub/home.mjs +1 -1
- package/hub/hub.mjs +12 -10
- package/hub/incoming.mjs +2 -0
- package/hub/linear.mjs +2 -2
- package/hub/live.mjs +2 -2
- package/hub/new-board.mjs +2 -2
- package/hub/permission-denied-hook.mjs +2 -0
- package/hub/pr-report.mjs +3 -0
- package/hub/presence.mjs +1 -1
- package/hub/prompts.mjs +1 -1
- package/hub/push.mjs +2 -2
- package/hub/readiness.mjs +2 -0
- package/hub/repos.mjs +3 -2
- package/hub/restart-board.mjs +1 -0
- package/hub/routes.mjs +1 -1
- package/hub/schedule.mjs +1 -1
- package/hub/sessions.mjs +2 -2
- package/hub/shell.mjs +2 -1
- package/hub/slack.mjs +2 -0
- package/hub/stream.mjs +12 -12
- package/hub/sw.js +2 -2
- package/hub/trust.mjs +2 -0
- package/hub/try.mjs +8 -3
- package/hub/upgrade.mjs +1 -0
- package/hub/usage.mjs +3 -3
- package/hub/views.mjs +1 -1
- package/package.json +5 -1
- package/shared/card-prs.mjs +1 -0
- package/shared/colors.css +9 -6
- package/shared/fake-gh.mjs +105 -0
- package/shared/repos.mjs +1 -1
- package/shared/service.mjs +16 -16
- package/shared/skills.mjs +2 -0
- package/shared/usage-gate.mjs +1 -1
- package/starter/public/claude.js +3 -2
- package/bin/web-ts.mjs +0 -2
- package/board/public/view-polling.js +0 -87
package/board/store.mjs
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
var
|
|
2
|
-
`).filter(Boolean).map(
|
|
3
|
-
`),"writeJSON");async function
|
|
4
|
-
`)}
|
|
5
|
-
`).
|
|
1
|
+
var hi=Object.defineProperty;var r=(e,t)=>hi(e,"name",{value:t,configurable:!0});import A from"node:fs/promises";import{constants as gi,existsSync as He}from"node:fs";import h from"node:path";import ft from"node:os";import{execFile as Gt}from"node:child_process";import{fileURLToPath as $i}from"node:url";import{BUILD_REFUSED as Zt,BUILDER_ASK as Ke,ITEM_STATUSES as Vt,MAX_IMPLEMENTERS as ki,lineupPhases as wt,mergeBlockers as yi,modelName as ze,noTakePicked as bi,ownerQuestion as Oe,plans as Je,PLANNER_WORD as xi,providerOf as pt,owesNext as mt,pushTag as vi,refOf as Qe,roundOf as ht,roundSettledBy as _i}from"./domain.mjs";import{GITHUB_STATE as Xt,appendEvent as De,listBoards as ke,samePath as Pe}from"../hub/boards.mjs";import{readUsageGate as Yt,pauseReason as en,builderPick as tn}from"../shared/usage-gate.mjs";import{reviseQueued as Si,flushQueue as ji,queueStatus as Oi,pushKindError as Di,readClaudeModel as Pi}from"../hub/push.mjs";import{holdLine as Ai,CHECK_MINUTES as Ri}from"../hub/schedule.mjs";import{AT_RE as Ni}from"../hub/duration.mjs";import{routeRecord as M}from"../hub/routes.mjs";import{ticketSource as gt,ticketIn as $t,resolveTicket as Ei,ticketTarget as Li,moveTicket as Ii,titleTickets as Bi,cardIdOf as Ci}from"./tickets.mjs";import{LINEAR_NEEDS_KEY as Ti,linearStatus as qi,linearWorks as Fi}from"../hub/linear.mjs";import{validateBranch as Mi}from"../hub/repos.mjs";import{withFileLock as kt}from"../hub/locks.mjs";import{fetchPR as nn,reportOf as an,githubLogin as Ui,postToPR as Wi,CLAUDE_PREFIX as sn,CLAUDE_MARK as Hi}from"../hub/pr-report.mjs";import{allLanded as on,cardPRs as Ge,hasPR as Ze,leastReady as Ki,parsePRRef as rn,prKeyOf as zi,prNames as Ae,prURL as Ji,storedPRs as cn}from"../shared/card-prs.mjs";import{reviewLinks as yt,topicItems as bt,reviewersOf as xt}from"./review.mjs";import{readiness as ln}from"../hub/readiness.mjs";import{slackDay as dn}from"../hub/slack.mjs";import"../bin/install.mjs";import{RuleError as Qi,rulesOf as un,addRule as Gi,editRule as Zi,removeRule as Vi,applyRules as Xi,readHubRules as Ve,writeHubRules as Yi}from"../hub/rules.mjs";class te extends Error{static{r(this,"BoardError")}}const l=r(e=>{throw new te(e)},"fail"),ea=r(()=>h.join(ft.homedir(),".claude","kickflip","sites"),"sitesDir"),Xe=r(()=>h.join(ft.homedir(),".claude","waves"),"wavesDir"),k=r(async(e,t)=>{try{return JSON.parse(await A.readFile(e,"utf8"))}catch{return t}},"readJSON"),W=r(async e=>{try{return(await A.readFile(e,"utf8")).split(`
|
|
2
|
+
`).filter(Boolean).map(t=>JSON.parse(t))}catch{return[]}},"readLines");async function vt(e,t){await A.mkdir(h.dirname(e),{recursive:!0});const i=`${e}.${process.pid}.tmp`;await A.writeFile(i,t),await A.rename(i,e)}r(vt,"replace");const x=r((e,t)=>vt(e,JSON.stringify(t,null,2)+`
|
|
3
|
+
`),"writeJSON");async function ee(e,t){await A.mkdir(h.dirname(e),{recursive:!0}),await A.appendFile(e,JSON.stringify(t)+`
|
|
4
|
+
`)}r(ee,"appendLine");async function Cs(e){/^[\w-]+$/.test(e||"")||l(`--site takes a route such as ios-orchestrator, not "${e}"`);const t=h.join(ea(),e);return await A.stat(t).then(i=>i.isDirectory(),()=>!1)||l(`no site at ${t}`),t}r(Cs,"siteDir");const Q=r(async e=>({title:"Board",items:"waves",...e?await k(h.join(e,"board.json"),{}):{}}),"boardConfig");async function T(e){const t=await Q(e);if(t.items==="waves")try{return(await A.readdir(Xe(),{withFileTypes:!0})).filter(n=>n.isDirectory()).map(n=>({slug:n.name,dir:h.join(Xe(),n.name),file:h.join(Xe(),n.name,"wave.json")}))}catch{return[]}const i=h.resolve(e,t.items);return[{slug:t.slug||h.basename(t.items,".json"),dir:h.dirname(i),file:i}]}r(T,"itemSources");const H=r((e,t)=>h.join(e,"data","threads",t.replace(/[^\w.-]+/g,"__")+".jsonl"),"threadFile"),L=r(e=>h.join(e,"data","owner.json"),"ownerFile"),D=r(e=>e||l("this command needs --site <route>"),"needSite"),S=r((e,t)=>typeof e=="string"&&e.trim()?e.trim():l(`${t} must be non-empty text`),"text"),Z=r((e,t)=>e===void 0?void 0:t(e),"optional"),ta=/^prototype-[\w-]+\.html(?:[?#]\S*)?$/,fn=r((e,t)=>typeof e=="string"&&(ta.test(e)||/^https?:\/\/\S+$/.test(e))?e:l(`${t} must be an http(s) URL or a site page such as prototype-<topic>.html?q=<id>`),"pageUrl"),_t=r((e,t)=>typeof e=="boolean"?e:l(`${t} must be true or false`),"bool"),re=r((e,t)=>typeof e=="string"&&/^[\w.-]+$/.test(e)?e:l(`${t} must use only letters, digits, "_", "." and "-"`),"domId"),Re=r((e,t,i)=>{(!e||typeof e!="object"||Array.isArray(e))&&l(`${i} must be a JSON object`);const n=Object.keys(e).filter(a=>!t.includes(a));n.length&&l(`${i} has unknown field ${n.map(a=>`"${a}"`).join(", ")}; allowed: ${t.join(", ")}`)},"onlyKeys");function B(e){const t=/^([^/\s]+)\/([^/\s]+)$/.exec(e||"");return t?{wave:t[1],id:t[2]}:l(`expected <wave>/<id>, not "${e}"`)}r(B,"parseKey");async function ye(e,t){const i=(await T(e)).find(a=>a.slug===t)||l(`this board has no wave "${t}"`),n=await k(i.file,null);return(!n||!Array.isArray(n.items))&&l(`${i.file} is not a wave file with items[]`),{source:i,wave:n}}r(ye,"loadWave");async function P(e,t){const{wave:i,id:n}=B(t);let a=await ye(e,i);!a.wave.items.some(o=>o.id===n)&&await za(e,t)&&(a=await ye(e,i));const s=a.wave.items.find(o=>o.id===n)||l(`wave "${i}" has no item "${n}"`);return{...a,item:s}}r(P,"loadItem");async function Ts(e,t,i){D(e),re(t,"the event id");const n=(await W(h.join(e,"data","events.jsonl"))).find(u=>u.id===t);n||l(`no event "${t}" in this site's data/events.jsonl`);const a=h.join(e,"data","replies",`${t}.html`);await vt(a,S(i,"the reply"));const s=n.kind==="ask"?`ask:${t}`:n.kind==="item-message"&&typeof n.data?.key=="string"?n.data.key:null,o=s?await mn(e,s,ua(i)):[],c=n.kind==="wave-triage"?["lineup","wave"].filter(u=>typeof n.data?.[u]=="string").map(u=>`${u}:${n.data[u]}`):[],d=n.kind==="ask"?`item:ask:${t}`:c[0];return d&&o.push(...await Cn(e,d,"Acted on: replied")),{file:a,lines:o}}r(Ts,"writeReply");const wn=r(e=>h.join(e,"data","working.json"),"workingFile");async function na(e){return(await k(wn(e),{})).events||{}}r(na,"readWorking");const qs=["ask","item-message","wave-triage","overnight-triage"];async function Fs(e,t){if(!t.length)return;const i=new Date().toISOString(),n=await na(e);await x(wn(e),{events:{...n,...Object.fromEntries(t.map(a=>[a,M(n,a)||i]))}})}r(Fs,"markWorking");const St=r(e=>h.join(e,"data","push-state.json"),"pushStateFile");async function ia(e,t){D(e);const i=Date.parse((await k(St(e),{})).last_push);if(Number.isNaN(i))return 0;const n=h.join(e,"data","replies"),a=(await A.readdir(n).catch(()=>[])).filter(o=>/\.(html|json)$/.test(o));return(await Promise.all(a.map(o=>A.stat(h.join(n,o)).then(c=>c.mtimeMs,()=>0)))).filter(o=>o>i&&o<=t.getTime()).length}r(ia,"repliesSincePush");async function aa(e,t){return await x(St(D(e)),{last_push:t.toISOString()}),St(e)}r(aa,"recordPush");const sa=h.join(h.dirname($i(import.meta.url)),"..","bin","kickflip.mjs");function oa(){if(!process.env.KICKFLIP_PUSH_CMD)return[process.execPath,sa,"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 te("KICKFLIP_PUSH_CMD must be a JSON array of strings")}r(oa,"pushCommand");async function Ms(e,t){const i=Di(t.kind);i&&l(i),t.title===void 0&&l("push needs --title");const n=new Date,a=ca(t.body,await ia(e,n)),s={kind:t.kind,title:t.title,body:a||void 0,url:fa(t.url),tag:t.tag},[o,...c]=oa(),d=[...c,...Object.entries(s).flatMap(([f,w])=>w===void 0?[]:[`--${f}`,w]),...t.urgent?["--urgent"]:[],...t.now?["--now"]:[]],u=await new Promise(f=>{Gt(o,d,{encoding:"utf8"},(w,m,g)=>f({error:w,said:`${m||""}${g||""}`.split(`
|
|
5
|
+
`).filter($=>$.trim())}))}),p=u.error?typeof u.error.code=="number"?u.error.code:1:0;if(p!==0)throw Object.assign(new te([...u.said,`kickflip push failed (${typeof u.error.code=="number"?`exit ${p}`:u.error.message}); the push was not recorded`].join(`
|
|
6
|
+
`)),{exitCode:p});return await aa(e,n),{lines:u.said}}r(Ms,"sendPush");const ra=/(?:^| · )(and \d+ repl(?:y|ies))$/,ca=r((e,t)=>[e,t&&`and ${t} ${t===1?"reply":"replies"}`].filter(Boolean).join(" \xB7 "),"withReplies"),pn=160;function la(e){const t=String(e).split(`
|
|
7
|
+
`).map(i=>i.replace(/\s+/g," ").trim()).find(Boolean)||"";return t.length>pn?`${t.slice(0,pn-1).trimEnd()}\u2026`:t}r(la,"pushLine");const da={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" "};function ua(e){return e.replace(/<details[\s\S]*?<\/details>/gi,`
|
|
6
8
|
`).replace(/<(?:br|\/?(?:p|div|li|ul|ol|h\d|blockquote|pre|tr|summary))\b[^>]*>/gi,`
|
|
7
|
-
`).replace(/<[^>]*>/g,"").replace(/&(?:#(x[\da-f]{1,6}|\d{1,7})|(\w+));/gi,(e,a,n)=>{const i=a&&parseInt(a.replace(/^x/i,""),/^x/i.test(a)?16:10);return a?i<=1114111?String.fromCodePoint(i):e:W(an,n.toLowerCase())??e})}s(on,"htmlText");function ea(t){if(!t||!t.startsWith("/"))return t;const e=new URL(t,"http://board");return/^#item=[^&]+$/.test(e.hash)&&(e.hash=`${e.hash}&reply`),/\/[\w-]+\.html$/.test(e.pathname)&&!e.pathname.endsWith("/index.html")&&e.searchParams.set("from","push"),e.pathname+e.search+e.hash}s(ea,"pushUrl");const sn=s((t,e)=>`/${f.basename(t)}/#item=${encodeURIComponent(e)}&reply`,"threadUrl");async function Vt(t,e,a){const n=nn(a);if(!n)return[];const i=en(e),o=await Be(i,r=>[n,tn.exec(r)?.[1]].filter(Boolean).join(" \xB7 "),sn(t,e));return o===null?[]:[`queued push ${i} now reads: ${o}`]}s(Vt,"freshenPush");const te=s(t=>f.join(t,"data","hidden.json"),"hiddenFile"),rn=s(async t=>new Set((await g(te(t),{events:[]})).events||[]),"readHidden");let ee=Promise.resolve();function na(t,e,a){P(e,"the event id");const n=ee.then(async()=>{const i=await rn(t);a?i.add(e):i.delete(e),await k(te(t),{events:[...i]})});return ee=n.catch(()=>{}),n}s(na,"setHidden");const xt=20*1024*1024,N=s(t=>f.join(t,"data","uploads"),"uploadsDir"),H=s((t,e)=>f.join(N(t),".meta",`${e}.json`),"uploadMeta"),ne=/^\d+-[\w.-]+$/,ae=/^[\w.+-]+\/[\w.+-]+$/,ie=s(()=>Object.assign(new I(`a file can be at most ${xt/1024/1024} MB`),{status:413}),"tooLarge");async function aa(t,e,{name:a,type:n,length:i}){if(v(t),i>xt)throw ie();const o=String(a||"").trim().slice(0,200)||"file",r=f.basename(o).replace(/[^\w.-]+/g,"-").replace(/^[.-]+/,"").slice(-80)||"file",l=String(n||"").split(";")[0].trim().toLowerCase(),d=ae.test(l)?l:"application/octet-stream";await S.mkdir(f.dirname(H(t,"x")),{recursive:!0});let p,u;for(let h=Date.now();!u;h++)p=`${h}-${r}`,u=await S.open(f.join(N(t),p),"wx").catch($=>$.code==="EEXIST"?null:Promise.reject($));const w=f.join(N(t),p);let m=0;try{for await(const h of e){if(m+=h.length,m>xt)throw ie();await u.write(h)}}catch(h){throw await u.close(),await S.rm(w,{force:!0}),h}return await u.close(),await k(H(t,p),{name:o,type:d,size:m,at:new Date().toISOString()}),{name:o,url:`uploads/${p}`,path:w,type:d,size:m}}s(aa,"saveUpload");async function ia(t,e){if(!ne.test(e||""))return null;const a=f.join(N(t),e),n=await S.stat(a).catch(()=>null);if(!n?.isFile())return null;const i=await g(H(t,e),{});return{path:a,name:i.name||e,type:ae.test(i.type||"")?i.type:"application/octet-stream",size:n.size}}s(ia,"readUpload");const oe=["github","slack"];async function cn(t,e,{text:a,link:n,who:i="claude",via:o}){if(v(t),String(e).startsWith("ask:"))await X(t,e.slice(4),"the ask");else{const{wave:d,id:p}=T(e),{wave:u}=await J(t,d);[...u.items,...u.followups||[]].some(w=>w.id===p)||c(`wave "${d}" has no item or follow-up "${p}"`)}/^[^\n]{1,40}$/.test(i.trim())||c("--who takes claude, love or a reviewer's name"),o!==void 0&&!oe.includes(o)&&c(`--via takes ${oe.join(" or ")}`);const r={ts:new Date().toISOString(),who:i.trim(),text:x(a,"--text"),link:_(n,d=>Gt(d,"--link")),via:o},l=M(t,e);return await K(l,r),{file:l,lines:r.who==="claude"&&!o?await Vt(t,e,r.text):[]}}s(cn,"appendThread");const z=s(t=>f.join(t,"data","ask-links.json"),"askLinksFile");async function ot(t){return(await g(z(t),{})).links||{}}s(ot,"readAskLinks");async function oa(t,e,a,n){v(t),P(e,"the event id"),(await R(f.join(t,"data","events.jsonl"))).some(l=>l.id===e&&l.kind==="ask")||c(`no ask "${e}" in this site's data/events.jsonl`),await St(t,e),await E(t,a);const o={ts:new Date().toISOString(),who:"claude",text:x(n,"--summary")},r=await ot(t);return await k(z(t),{links:{...r,[e]:a}}),await K(M(t,a),o),z(t)}s(oa,"relateAsk");async function sa(t,e){await X(t,e,"the ask");const a=await ot(t);return W(a,e)===void 0&&c(`ask "${e}" is not folded into a card`),delete a[e],await k(z(t),{links:a}),z(t)}s(sa,"unfoldAsk");const st=s(t=>f.join(t,"data","ask-progress.json"),"askProgressFile");async function rt(t){return(await g(st(t),{})).asks||{}}s(rt,"readAskProgress");async function St(t,e){const a=W(await rt(t),e)?.moved;a&&c(`ask "${e}" was moved to /${a.route}; it lives there now`)}s(St,"refuseMoved");async function ra(t,e,a,n){await X(t,e,"the ask"),await St(t,e);const i=await rt(t);return await k(st(t),{asks:{...i,[e]:{state:a,at:new Date().toISOString(),...n===void 0?{}:{next:x(n,"--next")}}}}),st(t)}s(ra,"markAsk");const ln=[void 0,"planned","waiting"];async function dn(t,e,a){const n=`ask "${e}"`;if(a.some(u=>u.kind==="ask-withdrawn"&&u.data?.id===e&&!u.data?.key))return{why:`${n} was deleted by the owner`};const i=W(await rt(t),e);if(i?.moved)return{why:`${n} was already moved to /${i.moved.route}`};const o=W(await ot(t),e);if(o)return{why:`${n} is folded into ${o}; run ask unfold ${e} first`};const r=[];for(const u of await F(t)){const w=await g(u.file,null);for(const m of Array.isArray(w?.items)?w.items:[])m.from_ask===e&&r.push({key:`${u.slug}/${m.id}`,it:m})}const l=r.find(({it:u})=>u.pr||!ln.includes(u.status));return l?{why:`${n} became ${l.key}, which has started`}:r.length?{items:r.map(u=>u.key)}:(await Promise.all(["html","json"].map(u=>S.stat(f.join(t,"data","replies",`${e}.${u}`)).then(()=>!0,()=>!1)))).some(Boolean)||!!i||a.some(u=>u.kind==="ask-done"&&u.data?.id===e)||(await R(M(t,`ask:${e}`))).some(u=>(u.who||"claude")==="claude"&&u.kind!=="status")?{why:`${n} is not New: Claude already answered it`}:null}s(dn,"askNotNew");async function un(t,e,a){const n=/^uploads\/([\w.-]+)$/.exec(a?.url||"")?.[1],i=ne.test(n||"")?f.join(N(t),n):null;if(!i||!await S.stat(i).then(d=>d.isFile(),()=>!1))return{name:String(a?.name||n||"file")};await S.mkdir(f.dirname(H(e,"x")),{recursive:!0});const o=n.replace(/^\d+-/,"");let r;for(let d=Date.now();r=`${d}-${o}`,!await S.copyFile(i,f.join(N(e),r),Pe.COPYFILE_EXCL).then(()=>!0,u=>u.code==="EEXIST"?!1:Promise.reject(u));d++);const l=await g(H(t,n),null);return l&&await k(H(e,r),l),{...a,url:`uploads/${r}`,path:f.join(N(e),r)}}s(un,"copyUpload");async function ca(t,e,a){v(t),P(e,"the event id"),(typeof a!="string"||!a.trim())&&c("ask move needs --to <route>, the board to move it to");const n=await R(f.join(t,"data","events.jsonl")),i=n.find(y=>y.id===e&&y.kind==="ask")||c(`no ask "${e}" in this site's data/events.jsonl`),o=zt(),r=o.find(y=>y.route===a)||c(`--to ${a}: no board at /${a} in the hub's manifest; the boards are ${o.map(y=>y.route).join(", ")||"none"}`);Kt(r.dir,t)&&c(`--to ${a}: that is this board`);const l=await dn(t,e,n);l?.why&&c(l.why);const d=n.filter(y=>y.kind==="ask-edited"&&y.data?.id===e&&typeof y.data?.text=="string").sort((y,C)=>String(y.ts).localeCompare(String(C.ts))).pop(),p=[];for(const y of i.data?.files||[])p.push(await un(t,r.dir,y));const u={route:f.basename(t),title:(await it(t)).title,id:e},w=d?d.data.text:i.data?.text||"",m=await Ht(r,"ask",{text:w,...p.length?{files:p}:{},from:u}),h={route:a,title:r.title,id:m},$=new Date().toISOString(),j=await rt(t);await k(st(t),{asks:{...j,[e]:{state:"done",at:$,moved:h}}});const O=`/${a}/#item=${encodeURIComponent(`ask:${m}`)}`;await K(M(t,`ask:${e}`),{ts:$,who:"claude",text:`Moved to ${r.title}.`,link:O});for(const y of l?.items||[])await vn(t,y,{status:"dropped",force:!0});return{file:f.join(r.dir,"data","events.jsonl"),lines:[`moved to ${O}`,...(l?.items||[]).map(y=>`dropped: ${y}`)]}}s(ca,"moveAsk");const se=s(t=>f.join(t,"data","checked.json"),"checkedFile");async function la(t,e,{idle:a=!1}={}){v(t),a&&e!==void 0&&c("pass --every or --idle, not both");const n=a?null:/^[1-9]\d{0,3}$/.test(String(e??"30"))?Number(e??"30"):c("--every takes the minutes between checks, such as 30");return await k(se(t),a?{at:new Date().toISOString(),every:null,idle:!0}:{at:new Date().toISOString(),every:n}),{file:se(t),lines:await fn()}}s(la,"writeChecked");async function fn(){const t=Ne(new Date);if(t.hold)return t.count?[`queued pushes held: ${Ce(t.hold)}`]:[];const e=await Fe();return e?[`sent ${e.sent} of ${e.count} queued push${e.count===1?"":"es"}${e.ok?"":"; the rest stay queued"}`]:[]}s(fn,"flushAfterTick");const ct=s(t=>f.join(t,"data","handshake.json"),"handshakeFile"),re=s(t=>g(ct(t),null),"readHandshake");async function da(t,e){v(t),P(e,"the event id");const a=(await R(f.join(t,"data","events.jsonl"))).find(i=>i.id===e);a?.kind!=="hello"&&c(`no hello event "${e}" in this site's data/events.jsonl`);const n=await re(t)||{};return n.hello?.id&&n.hello.id!==e&&c(`"${e}" is an older hello; ack the newest, "${n.hello.id}"`),await k(ct(t),{...n,hello:n.hello||{id:e,at:a.ts,session:null},ack:{id:e,at:new Date().toISOString()}}),ct(t)}s(da,"ackHello");async function ua(t){const e=await re(t);e&&!e.done_at&&await k(ct(t),{...e,done_at:new Date().toISOString()})}s(ua,"finishHandshake");const vt={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})$/},Z=s((t,e)=>t.trim().toLowerCase()===e.trim().toLowerCase(),"sameName");function Ot(t,e){return Array.isArray(t)||c(`${e} must be a JSON array of reviewers`),t.forEach((a,n)=>{const i=`${e}[${n}]`;Q(a,Object.keys(vt),i);for(const[o,r]of Object.entries(vt))(typeof a[o]!="string"||!r.test(a[o].trim()))&&c(`${i}.${o} must match ${r}`);t.slice(0,n).some(o=>Z(o.name,a.name))&&c(`${e} names "${a.name}" twice`)}),t.map(a=>Object.fromEntries(Object.keys(vt).map(n=>[n,a[n].trim()])))}s(Ot,"checkReviewers");async function At(t,e){const a=f.join(v(t),"board.json"),n=await g(a,{});return await k(a,{...n,reviewers:Ot(e(n.reviewers||[]),"the reviewers")}),a}s(At,"writeReviewers");const fa=s((t,e)=>At(t,()=>Ot(e,"the reviewer file")),"setReviewers");function pa(t,e){const a=Ot(Array.isArray(e)?e:[e],"the reviewer file");return At(t,n=>[...n.map(i=>a.find(o=>Z(o.name,i.name))||i),...a.filter(i=>!n.some(o=>Z(i.name,o.name)))])}s(pa,"addReviewers");function wa(t,e){return At(t,a=>{const n=e.filter(i=>!a.some(o=>Z(o.name,i)));return n.length&&c(`this board has no reviewer ${n.map(i=>`"${i}"`).join(", ")}`),a.filter(i=>!e.some(o=>Z(i.name,o)))})}s(wa,"removeReviewers");const pn=s(()=>f.join($t.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),Dt=s(t=>{try{return t()}catch(e){throw e instanceof He&&c(e.message),e}},"ruleCall"),Rt=s(async t=>Zt(await g(f.join(t,"board.json"),{})),"readBoardRules");let ce=Promise.resolve();function le(t,e){const a=ce.then(async()=>{const n=f.join(v(t),"board.json"),i=await g(n,{}),{set:o,...r}=e(i);return await k(n,{...i,...o}),{file:n,...r}});return ce=a.catch(()=>{}),a}s(le,"writeBoardJSON");const de=s((t,e)=>le(t,a=>{const n=Dt(()=>e(Zt(a)));return{set:{rules:n.rules},...n}}),"writeBoardRules"),ue=s(t=>{try{return t?yt(t):null}catch{return null}},"storedTickets"),_t=s(t=>{const e=ue(t?.tickets);return{auto_merge:t?.auto_merge===!0,auto_build:t?.auto_build===!0,...e?{tickets:e}:{}}},"settingsOf");async function ma(t){return _t(await g(f.join(v(t),"board.json"),{}))}s(ma,"readBoardSettings");function wn(t,e){Q(e,["auto_merge","auto_build","tickets"],"the settings"),Object.keys(e).length||c("the settings name nothing to change"),e={...e};for(const[a,n]of Object.entries(e))if(a==="tickets")try{e.tickets=yt(n)}catch(i){c(i.message)}else kt(n,a);return le(t,a=>{const n=_t(a),i={...n,...e};return{set:e,settings:i,changed:Object.keys(e).filter(o=>JSON.stringify(n[o])!==JSON.stringify(i[o]))}})}s(wn,"saveBoardSettings");async function ha(t,e,a){const n=String(e).replace(/-/g,"_");["auto_merge","auto_build"].includes(n)||c(`no board setting "${e}"; settings are ${Object.keys(_t({})).map(r=>r.replace(/_/g,"-")).join(", ")}`),["on","off"].includes(a)||c(`settings set ${e} takes on or off, not "${a}"`);const{file:i,settings:o}=await wn(t,{[n]:a==="on"});return{file:i,lines:[`${e} ${o[n]?"on":"off"}`]}}s(ha,"setBoardSetting");const $a=s(t=>Object.entries(t).map(([e,a])=>`setting ${e.replace(/_/g,"-")} ${e==="tickets"?JSON.stringify(a):a?"on":"off"}`),"settingsLines");async function Pt(t,e,a){const n=e?Dt(()=>a(nt())):await de(t,a);return e&&Dt(()=>Ye(n.rules)),{file:e?pn():n.file,lines:[`id ${n.rule.id}`]}}s(Pt,"changeRules");async function fe(t,e,a){const n=s(r=>r.map(l=>l.id),"ids");a||v(t);const[i,o]=a?[n(nt()),t?n(await Rt(t)):[]]:[n(await Rt(t)),n(nt())];i.includes(e)||c(`no ${a?"hub":"board"} rule "${e}"${o.includes(e)?`; it is a ${a?"board":"hub"} rule, so ${a?"drop":"pass"} --hub`:""}`)}s(fe,"checkRuleId");async function ya(t){const e=s(a=>n=>`${a} ${n.id} ${n.by==="claude"?"claude":"owner"} ${n.text}`,"line");return{lines:[...nt().map(e("hub")),...t?(await Rt(t)).map(e("board")):[]]}}s(ya,"listRules");function ga(t,{text:e,hub:a=!1,by:n}){return a||v(t),Pt(t,a,i=>ze(i,{text:e,by:n}))}s(ga,"addRuleTo");async function ka(t,e,{text:a,hub:n=!1}){return await fe(t,e,n),Pt(t,n,i=>Ke(i,e,a))}s(ka,"editRuleIn");async function ba(t,e,{hub:a=!1}){return await fe(t,e,a),Pt(t,a,n=>Qe(n,e))}s(ba,"removeRuleFrom");const ja=s((t,e,a)=>de(t,n=>Ze(n,e,a)),"saveBoardRules");async function xa(t,{id:e,title:a,summary:n}){v(t);const i=await g(D(t),{topics:[],items:[]}),o=i.topics||[];return o.some(r=>r.id===x(e,"the topic id"))&&c(`topic "${e}" already exists`),o.push({id:e.trim(),title:x(a,"--title"),...n===void 0?{}:{summary:x(n,"--summary")}}),await k(D(t),{...i,topics:o,items:i.items||[]}),D(t)}s(xa,"addTopic");const pe=["quiet","info","notice","danger","success"];function mn(t,e){if(t.diagram===void 0!=(t.practice===void 0)&&c(`${e} needs diagram and practice together`),t.diagram===void 0)return;const a=t.diagram;(!Array.isArray(a)||!a.length)&&c(`${e}.diagram must be a non-empty array of rows`),a.forEach((n,i)=>{(!Array.isArray(n)||!n.length||n.length>4)&&c(`${e}.diagram[${i}] must be a row of 1 to 4 boxes`),n.forEach((o,r)=>{const l=`${e}.diagram[${i}][${r}]`;Q(o,["text","tone","to"],l),x(o.text,`${l}.text`),_(o.tone,d=>pe.includes(d)?d:c(`${l}.tone takes ${pe.join(", ")}`)),_(o.to,d=>Array.isArray(d)&&i+1<a.length&&d.every(p=>Number.isInteger(p)&&p>=0&&p<a[i+1].length)?d:c(`${l}.to must list boxes of the next row by index`))})}),(!Array.isArray(t.practice)||!t.practice.length)&&c(`${e}.practice must be a non-empty array of lines`),t.practice.forEach((n,i)=>x(n,`${e}.practice[${i}]`))}s(mn,"checkDiagram");function hn(t,e){(!Array.isArray(t)||!t.length)&&c(`${e}.options must be a non-empty array`);const a=new Set;t.forEach((n,i)=>{const o=`${e}.options[${i}]`;Q(n,["id","label","recommended","detail","diagram","practice"],o),mn(n,o),a.has(x(n.id,`${o}.id`))&&c(`${e} has two options with id "${n.id}"`),a.add(n.id),x(n.label,`${o}.label`).startsWith(`${n.id}. `)&&c(`${o}.label starts with "${n.id}. ", but the page adds the option letter itself`),_(n.recommended,r=>kt(r,`${o}.recommended`)),_(n.detail,r=>typeof r=="string"?r:c(`${o}.detail must be text`))}),t.filter(n=>n.recommended).length>1&&c(`${e} recommends more than one option`)}s(hn,"checkOptions");const Y=s(t=>f.join(t,"data","prototypes.json"),"prototypesFile"),Tt=s((t,e,a)=>f.join(t,"public","prototypes",e,`${a}.js`),"takeFile");async function lt(t){const e=t?(await g(Y(t),{})).prototypes:null;return Array.isArray(e)?e:[]}s(lt,"readPrototypes");const $n=s(t=>typeof t=="string"&&/^[a-z0-9-]+$/.test(t)?t:c(`a prototype topic uses only a-z, 0-9 and "-", not "${t}"`),"prototypeTopic");function yn(t){const e=/^([^:]*):(.*)$/s.exec(t);(!e||!/^[A-Z]$/.test(e[1]))&&c(`--take takes KEY:"Label" with KEY one capital letter, not "${t}"`);const a=x(e[2],`--take ${e[1]}'s label`);return/^[A-Z]\.\s/.test(a)&&c(`--take ${e[1]}'s label starts with "${a.slice(0,3)}"; the take pill shows the label alone, so drop the letter`),{key:e[1],label:a}}s(yn,"prototypeTake");const G=s(t=>/^prototype-(.+)\.html$/.exec(f.posix.basename(String(t||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function Sa(t,{topic:e,title:a,card:n,takes:i=[]}){v(t),$n(e),x(a,"--title"),i.length||c('prototype add needs a --take KEY:"Label" for each take');const o=i.map(yn),r=o.find((p,u)=>o.findIndex(w=>w.key===p.key)!==u);r&&c(`--take ${r.key} is given twice`),n!==void 0&&await E(t,n);const l=await lt(t);l.some(p=>p.topic===e)&&c(`prototype ${e} is already recorded; run prototype rm ${e} first, or pick another topic`),await S.stat(f.join(t,"public",`prototype-${e}.html`)).then(()=>!0,()=>!1)&&c(`public/prototype-${e}.html is a hand-written page that questions may link; pick another topic`);const d={topic:e,title:a.trim(),...n===void 0?{}:{card:n},takes:o,at:new Date().toISOString()};return await k(Y(t),{prototypes:[...l,d]}),{file:Y(t),lines:[`page prototype-${e}.html`,...o.map(p=>`take ${p.key} ${Tt(t,e,p.key)}`)]}}s(Sa,"addPrototype");async function va(t){v(t);const e=await lt(t);return{lines:e.length?e.map(a=>`${a.topic} \xB7 ${a.title} \xB7 ${a.takes.map(n=>`${n.key}: ${n.label}`).join(", ")}${a.card?` \xB7 ${a.card}`:""}`):["no prototypes recorded"]}}s(va,"listPrototypes");async function Oa(t,e){v(t);const a=await lt(t);a.some(o=>o.topic===e)||c(`no prototype "${e}" is recorded`);const i=((await g(D(t),{items:[]})).items||[]).filter(o=>!o.resolved&&G(o.url)===e).map(o=>o.id);return i.length&&c(`prototype ${e} backs open question ${i.join(", ")}; resolve ${i.length===1?"it":"them"} first`),await k(Y(t),{prototypes:a.filter(o=>o.topic!==e)}),Y(t)}s(Oa,"removePrototype");async function gn(t,e,a){const n=e.url?.split(/[?#]/)[0];if(!n||!f.posix.basename(n).startsWith("prototype-"))return;const i=f.posix.basename(n),o=e.url.split("#")[0].split("?").slice(1).join("?"),r=`${a}.url`;new URLSearchParams(o).get("q")!==e.id&&c(`${r} must be ${i}?q=${e.id} so the pill can pick`);const l=G(n),d=(await lt(t)).find(m=>m.topic===l);d||c(`${r} names ${i}, which is not a recorded prototype; record it with board.mjs prototype add ${l} --title "\u2026" --take A:"\u2026", then write its take files`);const p=d.takes.map(m=>m.key),u=e.options.map(m=>m.id),w=u.map(m=>p.indexOf(m));w.some((m,h)=>m<0||h>0&&m<w[h-1])&&c(`${a}.options ids ${u.join(", ")} must be a non-empty subset of ${i}'s takes, in take order: ${p.join(", ")}`);for(const m of p)await S.stat(Tt(t,l,m)).then(h=>h.isFile(),()=>!1)||c(`${r} take ${m} has no file yet; write ${Tt(t,l,m)}`)}s(gn,"checkPrototype");async function Aa(t,e){v(t);const a=Array.isArray(e)?e:[e];a.length||c("the question file is empty");const n=await g(D(t),{topics:[],items:[]}),i=n.items||[],o=new Set(i.map(w=>w.id)),r=new Set((n.topics||[]).map(w=>w.id));for(const[w,m]of a.entries()){const h=`question ${m?.id??w+1}`;Q(m,["id","topic","source","question","context","url","urgent","options"],h),o.has(P(m.id,`${h}.id`))&&c(`question id "${m.id}" is already used`),o.add(m.id),x(m.topic,`${h}.topic`),m.topic.startsWith("item:ask:")?await X(t,m.topic.slice(9),`${h}.topic`):m.topic.startsWith("item:")?await E(t,m.topic.slice(5)):r.has(m.topic)||c(`topic "${m.topic}" is not declared; add it with: topic add ${m.topic} --title <t>`),x(m.question,`${h}.question`);for(const $ of["source","context"])_(m[$],j=>x(j,`${h}.${$}`));_(m.url,$=>Gt($,`${h}.url`)),_(m.urgent,$=>kt($,`${h}.urgent`)),hn(m.options,h),await gn(t,m,h)}const l=await we(t),d=new Date().toISOString(),p=[],u=[];for(const w of a){const m=G(w.url);for(const h of i)h.topic===w.topic&&(w.topic.startsWith("item:")&&Jt(h,l).state==="answered"?(Et(n,[h.id],`Followed by ${w.id}`,d),u.push(h.id)):!h.resolved&&m&&G(h.url)===m&&(Et(n,[h.id],`Replaced by ${w.id}`,d),p.push(h.id)))}return await k(D(t),{...n,topics:n.topics||[],items:[...i,...a]}),await Qt([...p,...u].map(w=>`q-${w}`),f.basename(t)),{file:D(t),lines:[...p.map(w=>`replaced: ${w}`),...u.map(w=>`resolved: ${w}`)]}}s(Aa,"addQuestions");function Et(t,e,a,n=new Date().toISOString()){for(const i of t.items||[])e.includes(i.id)&&Object.assign(i,{resolved:a,resolved_at:n})}s(Et,"setResolved");async function Lt(t,e,a,n){return Et(e,a,n),await k(D(t),e),await Qt(a.map(i=>`q-${i}`),f.basename(t)),D(t)}s(Lt,"markResolved");async function Da(t,e,a){v(t);const n=await g(D(t),{topics:[],items:[]});return(n.items||[]).some(i=>i.id===e)||c(`no question "${e}" in owner.json`),Lt(t,n,[e],x(a,"--note"))}s(Da,"resolveQuestion");async function we(t){const e=(await R(f.join(t,"data","events.jsonl"))).filter(a=>a.kind==="owner-answer");return Promise.all(e.map(async a=>{const n=await Promise.all(["html","json"].map(i=>S.stat(f.join(t,"data","replies",`${a.id}.${i}`)).then(o=>o.isFile(),()=>!1)));return{...a,reply:a.reply||n.some(Boolean)}}))}s(we,"ownerAnswers");async function me(t,e){const a=await g(D(t),{topics:[],items:[]}),n=await we(t),i=(a.items||[]).filter(o=>o.topic===`item:${e}`).map(o=>Jt(o,n));return{owner:a,asked:i}}s(me,"cardQuestions");async function kn(t,e,a){if(!t)return[];const{owner:n,asked:i}=await me(t,e),o=i.filter(r=>r.state==="answered").map(r=>r.id);return o.length&&await Lt(t,n,o,a),o.map(r=>`resolved: ${r}`)}s(kn,"resolveAnswered");async function he(t,e,a,n){if(!t)return async()=>[];const{owner:i,asked:o}=await me(t,e),r=o.filter(d=>d.state==="open").map(d=>d.id);r.length&&!n&&c(`${a}: ${e} has ${r.length===1?"an open question":"open questions"} ${r.join(", ")} with no answer yet; pass --force to close the card and resolve ${r.length===1?"it":"them"} too`);const l=o.filter(d=>d.state!=="resolved").map(d=>d.id);return async()=>(l.length&&await Lt(t,i,l,"Closed with the card"),l.map(d=>`resolved: ${d}`))}s(he,"closeQuestions");async function bn(t,e){const a=await g(D(t),{items:[]}),n=new Set((a.items||[]).filter(r=>r.topic===`item:${e}`&&G(r.url)).map(r=>r.id)),i=(await R(f.join(t,"data","events.jsonl"))).filter(r=>r.kind==="owner-answer"&&n.has(r.data?.id)).at(-1),o=String(i?.data?.note||"").replace(/\s+/g," ").trim();return o?{id:i.data.id,note:o}:null}s(bn,"unconfirmedPrototype");async function jn(t,e,a){const n=t?await bn(t,e):null;if(a===void 0)return n&&c(`${e} waits on a confirm round: the owner's answer to ${n.id} has a note ("${$e(n.note)}"); post the final version with prototype add and question add, or pass --confirmed "<the owner's words>" when that note says go`),null;const i=x(a,"--confirmed").replace(/\s+/g," ");return n||c(`${e} has no prototype answer with a note to confirm; drop --confirmed`),n.note.toLowerCase().includes(i.toLowerCase())||c(`--confirmed must quote the owner's note on ${n.id}: "${$e(n.note)}"`),`Building on the owner's go in their answer to ${n.id}: "${i}"`}s(jn,"confirmGate");const $e=s(t=>t.length>120?`${t.slice(0,119)}\u2026`:t,"clip");async function X(t,e,a="--from-ask"){return P(e,a),(await R(f.join(v(t),"data","events.jsonl"))).some(i=>i.id===e&&i.kind==="ask")?e:c(`no ask "${e}" in this site's data/events.jsonl`)}s(X,"checkAsk");const L=["done","merged","dropped"];function ye(t){return["building","fixing"].includes(t?.status)?(delete t.status,t.status_at=new Date().toISOString(),!0):!1}s(ye,"clearBuildingStatus");const dt=s((t,e)=>String(e).includes("/")?String(e):`${t}/${e}`,"depKey");async function ge(t,e,a){const n=(a.deps||[]).map(o=>dt(e,o)),i=await Promise.all(n.map(o=>E(t,o).then(({item:r})=>r,()=>null)));return n.filter((o,r)=>i[r]&&!L.includes(i[r].status))}s(ge,"unfinishedDeps");async function xn(t,e){const a=[];for(const n of await F(t)){const i=await g(n.file,null);for(const o of Array.isArray(i?.items)?i.items:[])![void 0,"planned","waiting","held"].includes(o.status)||!(o.deps||[]).some(r=>dt(n.slug,r)===e)||(await ge(t,n.slug,o)).length||a.push(`${n.slug}/${o.id}`)}return a}s(xn,"unblockedBy");async function Sn(t,e,a,n){const i=new Set(n.match(/[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),o=new Set((a.deps||[]).map(r=>dt(T(e).wave,r)));for(const r of await F(t)){const l=await g(r.file,null);for(const d of l?.items||[]){const p=`${r.slug}/${d.id}`;if(p===e||o.has(p)||L.includes(d.status))continue;[d.id,Te(l,d.id)].flatMap(w=>[w,`${r.slug}/${w}`]).some(w=>i.has(w))&&c(`--next names unfinished card ${p}; add it with --deps`)}}}s(Sn,"checkNextDeps");async function ut(t,e){const a=s(i=>c(`a notes file is a .md file inside ${t}, named relative to it: "${e}" ${i}`),"bad");(typeof e!="string"||!e.trim())&&a("is empty"),(f.isAbsolute(e)||e.split(/[\\/]/).includes(".."))&&a("leaves the folder"),f.extname(e).toLowerCase()!==".md"&&a("is not a .md file");const n=await S.realpath(f.resolve(t,e)).catch(()=>a("does not exist"));return n.startsWith(await S.realpath(t)+f.sep)||a("leaves the folder"),n}s(ut,"planPath");async function Ra(t,e){const{source:a,wave:n,item:i}=await E(t,e);if(i.plan)return{wave:n,item:i,markdown:await S.readFile(await ut(a.dir,i.plan),"utf8")};if(typeof i.notes=="string"&&i.notes.trim())return{wave:n,item:i,markdown:i.notes};c(`${e} has no notes`)}s(Ra,"readNotes");function ke({install:t,installSha:e,installLabel:a,clearInstall:n}){if(t===void 0)return(e!==void 0||a!==void 0)&&c("--install-sha and --install-label go with --install <url>"),n?null:void 0;n&&c("pass --install or --clear-install, not both");let i;try{i=new URL(String(t).trim())}catch{i=null}(!i||!["https:","itms-services:"].includes(i.protocol))&&c(`--install takes an https:// or itms-services:// URL, not "${t}"`),i.protocol==="itms-services:"&&!(i.searchParams.get("action")==="download-manifest"&&/^https:\/\//.test(i.searchParams.get("url")||""))&&c("--install: an itms-services:// URL needs ?action=download-manifest&url=https://\u2026/manifest.plist");const o=e===void 0?void 0:/^[0-9a-f]{7,40}$/i.test(e)?e.toLowerCase():c(`--install-sha takes a commit sha of 7 to 40 hex digits, not "${e}"`);return{url:String(t).trim(),...a===void 0?{}:{label:x(a,"--install-label")},built_at:new Date().toISOString(),...o?{sha:o}:{}}}s(ke,"installOf");async function _a(t,{install:e,installSha:a}){const n=ke({install:e,installSha:a});n||c("main set needs --install <url>");const i=f.join(v(t),"data","main.json");return await k(i,{install:n}),{file:i,lines:["main install set"]}}s(_a,"setMainInstall");async function be(t,e,{ticket:a,status:n,clearStatus:i,next:o,clearNext:r,pr:l,fromAsk:d,held:p,unheld:u,notesFile:w,deps:m,install:h,installSha:$,installLabel:j,clearInstall:O,title:y,force:C,confirmed:Ft},Nt){n!==void 0&&i&&c("pass --status or --clear-status, not both"),Ft!==void 0&&n!=="building"&&c("--confirmed goes with --status building"),C&&!L.includes(n)&&c(`--force goes with --status ${L.join(", ")}`),o!==void 0&&r&&c("pass --next or --clear-next, not both"),typeof o=="string"&&!o.trim()&&([o,r]=[void 0,!0]),p&&u&&c("pass --held or --unheld, not both");const et=ke({install:h,installSha:$,installLabel:j,clearInstall:O});[n,i,o,r,l,d,p,u,w,m,et,y,a].every(A=>A===void 0||A===!1)&&c("nothing to set: pass --status, --clear-status, --next, --clear-next, --pr, --from-ask, --held, --unheld, --notes-file, --deps, --install, --clear-install, --title or --ticket");const B=d===void 0?void 0:await X(t,d);B&&await St(t,B);const{source:q,wave:mt,item:b}=await E(t,e),Ae=L.includes(b.status);if(w!==void 0&&(await ut(q.dir,w),b.plan=f.normalize(w)),y!==void 0&&(b.title=x(y,"--title")),m!==void 0){const A=await je(t,T(e).wave,m);A.length?b.deps=A:delete b.deps}if(p&&(b.held=!0),u&&(delete b.held,b.status==="held"&&delete b.status),(p||u)&&(b.held_at=new Date().toISOString()),n==="building"&&(b.held||b.status==="held")&&c(`${e} is held; start it only once the owner resumes it, with item set ${e} --unheld`),n!==void 0&&(b.status=Mt.includes(n)?n:c(`--status takes ${Mt.join(", ")}`)),n==="building"){const A=await ge(t,T(e).wave,b);A.length&&c(`${e} is blocked by ${A.join(", ")}; start it once ${A.length===1?"that is":"those are"} done or merged`)}const Ct=n==="building"?await jn(t,e,Ft):null,De=L.includes(n)?await he(t,e,`--status ${n}`,C):async()=>[];i&&delete b.status,(n!==void 0||i)&&(b.status_at=new Date().toISOString()),o!==void 0&&(b.next=x(o,"--next"),await Sn(t,e,b,b.next)),r&&delete b.next,l!==void 0&&(b.pr=/^[1-9]\d*$/.test(String(l))?Number(l):c("--pr takes a PR number"),ye(b)),B&&(b.from_ask=B),et&&(b.install=et),et===null&&delete b.install;const qt=a!==void 0||B&&!b.ticket&&!b.linear?.length?await V(t,e,mt,b,a,Nt,q.dir,ft(t,e)):null;await k(q.file,mt);const U=await De();w!==void 0&&U.push(...await kn(t,e,"Recorded in the card's notes")),Ct&&U.push(...(await cn(t,e,{text:Ct})).lines),qt&&U.push(qt);const Ut=Ue({status:n,pr:l}),Wt=Ut&&await We(await Se(t),b,Ut,{...Nt,repo:xe(t,mt)});Wt&&U.push(Wt),(l!==void 0||Rn.includes(n))&&await _n(q.dir,b.id,L.includes(n));const ht=B?await ot(t):{};if(W(ht,B)!==void 0&&(delete ht[B],await k(z(t),{links:ht})),!L.includes(n)||Ae)return{file:q.file,lines:U};const It=await xn(t,e),Re=new Date().toISOString();for(const A of It)await K(M(t,A),{ts:Re,who:"claude",text:`Unblocked: ${b.id} is ${n}.`}),await Ht({dir:t},"card-unblocked",{key:A});return{file:q.file,lines:[...It.map(A=>`unblocked: ${A}`),...U]}}s(be,"changeItem");async function je(t,e,a){const n=[...new Set(String(a).split(",").map(i=>i.trim()).filter(Boolean))];for(const i of n)await E(t,dt(e,i));return n}s(je,"depList");const xe=s((t,e)=>t&&zt().find(a=>Kt(a.dir,t))?.github||e.repo,"boardRepoOf"),Se=s(async t=>ue((await it(t)).tickets),"boardSource");async function V(t,e,a,n,i,o,r,l){try{const d=await it(t),p=yt(d.tickets||{source:"internal"}),u=s(($,j)=>$.internal===!0?String(j.id).replace(/^S(\d+)$/,`${p.prefix}-$1`):null,"ownId");i===void 0&&p.source==="internal"&>(p,u(a,n))&&(i=u(a,n));let w=0;if(p.source==="internal")for(const $ of await F(t)){const j=await g($.file,{});for(const O of[...j.items||[],...j.followups||[]]){const y=[O.ticket,u(j,O)];if(w=Math.max(w,...y.map(C=>gt(p,C)||0)),i!==void 0&&`${$.slug}/${O.id}`!==e&&y.includes(i))throw Object.assign(new I(`${i} is already the ticket of ${$.slug}/${O.id}`),{held:!0})}}const m=i===void 0&&p.source!=="internal"&&n.plan?await S.readFile(await ut(r,n.plan),"utf8"):n.notes||"",h=await qe(p,{ticket:i,title:n.title,body:m,repo:xe(t,a),next:w+1},o);return h.mcp?(delete n.ticket,delete n.ticket_created,Me(p,n.title,l)):(n.ticket=h.ticket,h.created&&p.source!=="internal"?n.ticket_created=!0:delete n.ticket_created,null)}catch(d){if(d instanceof I)throw d;c(d.message)}}s(V,"assignTicket");const tt=s((t,e)=>Je(f.join(t||at(),"tickets"),e,{heartbeat:!0}),"ticketWrite"),vn=s((t,e,a,n)=>a.ticket!==void 0||a.fromAsk!==void 0?tt(t,()=>be(t,e,a,n)):be(t,e,a,n),"setItem"),Pa=s((t,e,a,n)=>tt(t,()=>On(t,e,a,n)),"addItem"),Ta=s((t,e,a,n)=>tt(t,()=>An(t,e,a,n)),"addFollowup");async function On(t,e,{title:a,notes:n,deps:i,notesFile:o,ticket:r},l){const{wave:d,id:p}=T(e);P(p,"the item id");const{source:u,wave:w}=await J(t,d);w.items.some($=>$.id===p)&&c(`wave "${d}" already has an item "${p}"`);const m={id:p,title:x(a,"--title"),created:new Date().toISOString()};n!==void 0&&(m.notes=x(n,"--notes")),i!==void 0&&(m.deps=await je(t,d,i)),o!==void 0&&(await ut(u.dir,o),m.plan=f.normalize(o)),m.status=m.deps?.length?"waiting":"planned";const h=await V(t,e,w,m,r,l,u.dir,ft(t,e));return w.items.push(m),await k(u.file,w),{file:u.file,lines:h?[h]:[]}}s(On,"createItem");async function An(t,e,{id:a,title:n,from:i,ticket:o},r){const{source:l,wave:d}=await J(t,e);P(a,"--id");const p=d.followups||[];p.some(m=>m.id===a)&&c(`wave "${e}" already has follow-up ${a}`);const u={id:a,title:x(n,"--title"),...i===void 0?{}:{from:x(i,"--from")},filed:new Date().toISOString().slice(0,10)},w=await V(t,`${e}/${a}`,d,u,o,r,l.dir,ft(t,`${e}/${a}`,!0));return p.push(u),await k(l.file,{...d,followups:p}),{file:l.file,lines:w?[w]:[]}}s(An,"createFollowup");const Ea=s((t,e,a,n)=>tt(t,async()=>{const{source:i,wave:o}=await J(t,e),r=(o.followups||[]).find(l=>l.id===a)||c(`wave "${e}" has no follow-up "${a}"`);return await V(t,`${e}/${a}`,o,r,x(n,"--ticket"),void 0,i.dir),await k(i.file,o),i.file}),"setFollowupTicket"),ft=s((t,e,a=!1)=>{const n=t?`--site ${f.basename(t)} `:"";return a?`board.mjs ${n}followup set ${e.replace("/"," ")}`:`board.mjs ${n}item set ${e}`},"setTicketCommand"),La=s((t,{dryRun:e=!1}={},a)=>tt(v(t),()=>Dn(t,e,a)),"backfillTickets");async function Dn(t,e,a){const n=await Se(t)||c("this board has no ticket source yet; pick one in Board settings first"),i=new Set,o=[];for(const p of await F(t)){const u=await g(p.file,null);if(Array.isArray(u?.items)){o.push({entry:p,wave:u});for(const w of[...u.items,...u.followups||[]])w.ticket&&i.add(w.ticket),u.internal===!0&&n.source==="internal"&&i.add(String(w.id).replace(/^S(\d+)$/,`${n.prefix}-$1`))}}const r=[],l={linked:0,created:0,failed:0,mcp:0};for(const{entry:p,wave:u}of o)for(const w of[...u.items,...u.followups||[]]){const m=!u.items.includes(w);if(w.ticket||(m?w.done:L.includes(w.status)))continue;const h=`${p.slug}/${w.id}`,$=[...Ie(n,w.title),...(w.linear||[]).filter(j=>gt(n,j))].find(j=>n.source!=="internal"||!i.has(j));if(e){l[$?"linked":"created"]++,r.push(`${h} would ${$?`link ${$}`:"get a new ticket"}`);continue}try{const j=await V(t,h,u,w,$,a,p.dir,ft(t,h,u.items.includes(w)===!1));if(j){l.mcp++,r.push(`${h} ${j}`);continue}await k(p.file,u),i.add(w.ticket),l[$?"linked":"created"]++,r.push(`${h} ${$?"linked":"created"} ${w.ticket}`)}catch(j){if(!(j instanceof I))throw j;l.failed++,r.push(`${h} failed: ${j.message}`)}}const d=e?`would link ${l.linked}, create ${l.created}`:`linked ${l.linked}, created ${l.created}, failed ${l.failed}${l.mcp?`, ${l.mcp} to create through the Linear MCP`:""}`;return{lines:[...r,`backfill${e?" (dry run)":""}: ${d}`]}}s(Dn,"backfill");async function Ba(t,e,a,{force:n}={}){const{source:i,wave:o}=await J(t,e),r=(o.followups||[]).find(d=>d.id===a)||c(`wave "${e}" has no follow-up "${a}"`),l=await he(t,`${e}/${a}`,"followup done",n);return r.done=!0,await k(i.file,o),{file:i.file,lines:await l()}}s(Ba,"finishFollowup");async function pt(t,e,a,n="main"){const{id:i}=T(e);/^[\w-]{1,40}$/.test(n)||c(`--agent takes a name of letters, digits, "_" and "-", not "${n}"`);const o=n==="main"?`${i}.json`:`${i}@${n}.json`;if(a)return f.join(f.resolve(a),"agents",o);const{source:r}=await E(t,e);return f.join(r.dir,"agents",o)}s(pt,"builderFile");const ve=s((t,e)=>typeof t=="string"&&/^\d{4}-\d\d-\d\dT/.test(t)&&!Number.isNaN(Date.parse(t))?new Date(t).toISOString():c(`${e} takes an ISO time such as 2026-10-23T16:30:00-07:00`),"isoTime"),Oe=s((t,e,a)=>t?x(e,a).slice(0,160):c(`${a} needs --site, since the line goes to the item's thread`),"progressLine");async function Bt(t,e,a="main",n,i,o){const r=a==="main"?"":n||a,l=r?`${r[0].toUpperCase()}${r.slice(1)}: ${i}`:i,d=M(t,e);(await R(d)).filter(p=>p.kind==="status").pop()?.text!==l&&await K(d,{ts:o,who:"claude",kind:"status",text:l})}s(Bt,"statusToThread");async function Fa(t,e,{label:a,progress:n,dir:i,agent:o="main",kind:r,eta:l,worktree:d}){const p=Ee();if(p.state==="paused")throw new I(Le(p));const u=_(n,O=>Oe(t,O,"--progress")),w=await pt(t,e,i,o),m=new Date().toISOString(),h=_(r,O=>/^[\w.-]{1,30}$/.test(O)?O:c("--kind takes a short word such as opus or codex")),$={label:a===void 0?"Builder":x(a,"--label"),...h?{kind:h}:{},started:m,...l===void 0?{}:{eta:ve(l,"--eta")}},j=d?await d():null;return await k(w,j?{...$,worktree:j}:$),u&&await Bt(t,e,o,h,u,m),{file:w,lines:j?[`worktree: ${j.path}`]:[]}}s(Fa,"startBuilder");async function Na(t,e,{dir:a}={}){const{id:n}=T(e),i=f.dirname(await pt(t,e,a)),o=await wt(f.dirname(i),n);return(await Promise.all(o.map(l=>g(l,null)))).filter(l=>l?.worktree?.path).sort((l,d)=>String(d.started).localeCompare(String(l.started)))[0]?.worktree??null}s(Na,"recordedWorktree");async function Ca(t,e,a,{dir:n,agent:i="main",eta:o,clearEta:r}){o!==void 0&&r&&c("pass --eta or --clear-eta, not both");const l=Oe(t,a,"builder progress"),d=await pt(t,e,n,i),p=new Date().toISOString(),{progress:u,progress_at:w,done:m,...h}=await g(d,{label:"Builder",started:p});return o!==void 0&&(h.eta=ve(o,"--eta")),r&&delete h.eta,await k(d,h),await Bt(t,e,i,h.kind,l,p),d}s(Ca,"builderProgress");const Rn=["checking","done","merged","dropped"];async function wt(t,e){const a=f.join(t,"agents");return(await S.readdir(a).catch(()=>[])).filter(n=>n===`${e}.json`||n.startsWith(`${e}@`)&&n.endsWith(".json")).map(n=>f.join(a,n))}s(wt,"builderFiles");async function _n(t,e,a){const n=new Date().toISOString();for(const i of await wt(t,e)){const o=await g(i,null);if(!o||o.done||o.kind==="install"&&!a)continue;const{eta:r,...l}=o;await k(i,{...l,done:n})}}s(_n,"closeBuilders");async function Pn(t,e,{dir:a,agent:n,startedBetween:i,note:o}){const r=await pt(t,e,a,n),l=await g(r,null);if(!l||l.done)return r;if(i){const y=Date.parse(l.started);if(!(y>=i[0]&&y<=i[1]))return r}const{eta:d,...p}=l,u=new Date().toISOString();await k(r,{...p,done:u,...o?{note:o}:{}}),o&&await Bt(t,e,n,l.kind,o,u);const{id:w}=T(e),m=f.dirname(f.dirname(r));if((await Promise.all((await wt(m,w)).map(y=>g(y,null)))).some(y=>y?.started&&!y.done))return r;const $=a?{file:f.join(m,"wave.json")}:(await E(t,e)).source,j=await g($.file,null),O=j?.items?.find(y=>y.id===w);return ye(O)&&await k($.file,j),r}s(Pn,"stopBuilder");async function Tn(t){const e=[];for(const a of await F(t)){const n=await g(a.file,null);for(const i of n?.items||[])for(const o of await wt(a.dir,i.id)){const r=await g(o,null);if(!r?.started||r.done)continue;const l=f.basename(o,".json");e.push({key:`${a.slug}/${i.id}`,agent:l.slice(String(i.id).length+1)||"main",label:r.label||"Builder",started:r.started})}}return e}s(Tn,"openBoardBuilders");async function qa(t){const e=await Tn(t);for(const a of e)await Pn(t,a.key,{agent:a.agent,note:"Board closed. Builder record stopped; work is kept for reopening."});return e}s(qa,"closeBoardBuilders");export{I as BoardError,Xt as PUSH_LINE_MAX,xt as UPLOAD_LIMIT,da as ackHello,Ta as addFollowup,Pa as addItem,Sa as addPrototype,Aa as addQuestions,pa as addReviewers,ga as addRuleTo,xa as addTopic,cn as appendThread,La as backfillTickets,it as boardConfig,Ca as builderProgress,qa as closeBoardBuilders,ka as editRuleIn,Ba as finishFollowup,ua as finishHandshake,te as hiddenFile,F as itemSources,va as listPrototypes,ya as listRules,ra as markAsk,ca as moveAsk,Tn as openBoardBuilders,T as parseKey,nn as pushLine,en as pushTag,ea as pushUrl,ot as readAskLinks,rt as readAskProgress,Rt as readBoardRules,ma as readBoardSettings,re as readHandshake,rn as readHidden,g as readJSON,Ra as readNotes,lt as readPrototypes,ia as readUpload,Ve as readWorking,Vn as recordPush,Na as recordedWorktree,oa as relateAsk,Oa as removePrototype,wa as removeReviewers,ba as removeRuleFrom,Xn as repliesSincePush,Da as resolveQuestion,ja as saveBoardRules,wn as saveBoardSettings,aa as saveUpload,ha as setBoardSetting,Ea as setFollowupTicket,na as setHidden,vn as setItem,_a as setMainInstall,fa as setReviewers,Gn as setWorking,$a as settingsLines,_t as settingsOf,Zn as siteDir,Ge as sitesDir,Fa as startBuilder,Pn as stopBuilder,Tt as takeFile,M as threadFile,sa as unfoldAsk,at as wavesDir,ta as withReplies,la as writeChecked,Yn as writeReply};
|
|
9
|
+
`).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:M(da,n.toLowerCase())??t})}r(ua,"htmlText");function fa(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(fa,"pushUrl");const wa=r((e,t)=>`/${h.basename(e)}/#item=${encodeURIComponent(t)}&reply`,"threadUrl");async function mn(e,t,i){const n=la(i);if(!n)return[];const a=vi(t),s=await Si(a,o=>[n,ra.exec(o)?.[1]].filter(Boolean).join(" \xB7 "),wa(e,t));return s===null?[]:[`queued push ${a} now reads: ${s}`]}r(mn,"freshenPush");const hn=r(e=>h.join(e,"data","hidden.json"),"hiddenFile"),pa=r(async e=>new Set((await k(hn(e),{events:[]})).events||[]),"readHidden");let gn=Promise.resolve();function Us(e,t,i){re(t,"the event id");const n=gn.then(async()=>{const a=await pa(e);i?a.add(t):a.delete(t),await x(hn(e),{events:[...a]})});return gn=n.catch(()=>{}),n}r(Us,"setHidden");const jt=20*1024*1024,we=r(e=>h.join(e,"data","uploads"),"uploadsDir"),be=r((e,t)=>h.join(we(e),".meta",`${t}.json`),"uploadMeta"),$n=/^\d+-[\w.-]+$/,kn=/^[\w.+-]+\/[\w.+-]+$/,yn=r(()=>Object.assign(new te(`a file can be at most ${jt/1024/1024} MB`),{status:413}),"tooLarge");async function Ws(e,t,{name:i,type:n,length:a}){if(D(e),a>jt)throw yn();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=kn.test(c)?c:"application/octet-stream";await A.mkdir(h.dirname(be(e,"x")),{recursive:!0});let u,p;for(let m=Date.now();!p;m++)u=`${m}-${o}`,p=await A.open(h.join(we(e),u),"wx").catch(g=>g.code==="EEXIST"?null:Promise.reject(g));const f=h.join(we(e),u);let w=0;try{for await(const m of t){if(w+=m.length,w>jt)throw yn();await p.write(m)}}catch(m){throw await p.close(),await A.rm(f,{force:!0}),m}return await p.close(),await x(be(e,u),{name:s,type:d,size:w,at:new Date().toISOString()}),{name:s,url:`uploads/${u}`,path:f,type:d,size:w}}r(Ws,"saveUpload");async function Hs(e,t){if(!$n.test(t||""))return null;const i=h.join(we(e),t),n=await A.stat(i).catch(()=>null);if(!n?.isFile())return null;const a=await k(be(e,t),{});return{path:i,name:a.name||t,type:kn.test(a.type||"")?a.type:"application/octet-stream",size:n.size}}r(Hs,"readUpload");const bn=["github","slack"];async function Ne(e,t,{text:i,link:n,log:a,who:s="claude",via:o}){D(e),String(t).startsWith("ask:")?await Be(e,t.slice(4),"the ask"):await P(e,t),/^[^\n]{1,40}$/.test(s.trim())||l("--who takes claude, love or a reviewer's name"),o!==void 0&&!bn.includes(o)&&l(`--via takes ${bn.join(" or ")}`);const c={ts:new Date().toISOString(),who:s.trim(),text:S(i,"--text"),link:Z(n,u=>fn(u,"--link")),log:a,via:o},d=H(e,t);return await ee(d,c),{file:d,lines:c.who==="claude"&&!o?await mn(e,t,c.text):[]}}r(Ne,"appendThread");const ce=r(e=>h.join(e,"data","ask-links.json"),"askLinksFile");async function pe(e){return(await k(ce(e),{})).links||{}}r(pe,"readAskLinks");async function ne(e,t){const{tickets:i}=me(await Q(e)),n=await T(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 k(c.file,null);if(!d)return o[2];const u=(d.items||[]).find(p=>p.id===o[2]);return Qe(d,u||{id:o[2]},i)}const s=e?M(await pe(e),a):void 0;if(s)return ne(e,s);for(const o of n){const c=await k(o.file,null),d=(Array.isArray(c?.items)?c.items:[]).find(u=>u.from_ask===a);if(d)return Qe(c,d,i)}return`task ${a}`}r(ne,"cardName");async function Ks(e,t){const i=(await W(h.join(e,"data","events.jsonl"))).find(n=>n.id===t);return i?.kind==="ask"?ne(e,`ask:${t}`):typeof i?.data?.key=="string"?ne(e,i.data.key):`the ${i?.kind} event`}r(Ks,"eventCard");async function zs(e,t,i,n){D(e),re(t,"the event id"),(await W(h.join(e,"data","events.jsonl"))).some(c=>c.id===t&&c.kind==="ask")||l(`no ask "${t}" in this site's data/events.jsonl`),await Ot(e,t),await P(e,i);const s={ts:new Date().toISOString(),who:"claude",text:S(n,"--summary")},o=await pe(e);return await x(ce(e),{links:{...o,[t]:i}}),await ee(H(e,i),s),ce(e)}r(zs,"relateAsk");async function Js(e,t){await Be(e,t,"the ask");const i=await pe(e);return M(i,t)===void 0&&l(`ask "${t}" is not folded into a card`),delete i[t],await x(ce(e),{links:i}),ce(e)}r(Js,"unfoldAsk");const Ye=r(e=>h.join(e,"data","ask-progress.json"),"askProgressFile");async function et(e){return(await k(Ye(e),{})).asks||{}}r(et,"readAskProgress");async function Ot(e,t){const i=M(await et(e),t)?.moved;i&&l(`ask "${t}" was moved to /${i.route}; it lives there now`)}r(Ot,"refuseMoved");async function Qs(e,t,i,n){await Be(e,t,"the ask"),await Ot(e,t);const a=await et(e);return await x(Ye(e),{asks:{...a,[t]:{state:i,at:new Date().toISOString(),...n===void 0?{}:{next:S(n,"--next")}}}}),Ye(e)}r(Qs,"markAsk");const ma=[void 0,"planned","waiting"];async function ha(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=M(await et(e),t);if(a?.moved)return{why:`${n} was already moved to /${a.moved.route}`};const s=M(await pe(e),t);if(s)return{why:`${n} is folded into ${s}; run ask unfold ${t} first`};const o=[];for(const p of await T(e)){const f=await k(p.file,null);for(const w of Array.isArray(f?.items)?f.items:[])w.from_ask===t&&o.push({key:`${p.slug}/${w.id}`,it:w})}const c=o.find(({it:p})=>Ze(p)||!ma.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=>A.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 W(H(e,`ask:${t}`))).some(p=>(p.who||"claude")==="claude"&&p.kind!=="status")?{why:`${n} is not New: Claude already answered it`}:null}r(ha,"askNotNew");async function ga(e,t,i){const n=/^uploads\/([\w.-]+)$/.exec(i?.url||"")?.[1],a=$n.test(n||"")?h.join(we(e),n):null;if(!a||!await A.stat(a).then(d=>d.isFile(),()=>!1))return{name:String(i?.name||n||"file")};await A.mkdir(h.dirname(be(t,"x")),{recursive:!0});const s=n.replace(/^\d+-/,"");let o;for(let d=Date.now();o=`${d}-${s}`,!await A.copyFile(a,h.join(we(t),o),gi.COPYFILE_EXCL).then(()=>!0,p=>p.code==="EEXIST"?!1:Promise.reject(p));d++);const c=await k(be(e,n),null);return c&&await x(be(t,o),c),{...i,url:`uploads/${o}`,path:h.join(we(t),o)}}r(ga,"copyUpload");async function Gs(e,t,i){D(e),re(t,"the event id"),(typeof i!="string"||!i.trim())&&l("ask move needs --to <route>, the board to move it to");const n=await W(h.join(e,"data","events.jsonl")),a=n.find(_=>_.id===t&&_.kind==="ask")||l(`no ask "${t}" in this site's data/events.jsonl`),s=ke(),o=s.find(_=>_.route===i)||l(`--to ${i}: no board at /${i} in the hub's manifest; the boards are ${s.map(_=>_.route).join(", ")||"none"}`);Pe(o.dir,e)&&l(`--to ${i}: that is this board`);const c=await ha(e,t,n);c?.why&&l(c.why);const d=n.filter(_=>_.kind==="ask-edited"&&_.data?.id===t&&typeof _.data?.text=="string").sort((_,R)=>String(_.ts).localeCompare(String(R.ts))),u=d.at(-1),p=[];for(const _ of[...a.data?.files||[],...d.flatMap(R=>R.data.files||[])])p.push(await ga(e,o.dir,_));const f={route:h.basename(e),title:(await Q(e)).title,id:t},w=u?u.data.text:a.data?.text||"",m=await De(o,"ask",{text:w,...p.length?{files:p}:{},from:f}),g={route:i,title:o.title,id:m},$=new Date().toISOString(),y=await et(e);await x(Ye(e),{asks:{...y,[t]:{state:"done",at:$,moved:g}}});const v=`/${i}/#item=${encodeURIComponent(`ask:${m}`)}`;await ee(H(e,`ask:${t}`),{ts:$,who:"claude",text:`Moved to ${o.title}.`,link:v});for(const _ of c?.items||[])await Se(e,_,{status:"dropped",force:!0,text:`Dropped: its ask moved to ${o.title}.`});return{file:h.join(o.dir,"data","events.jsonl"),lines:[`moved to ${v}`,...(c?.items||[]).map(_=>`dropped: ${_}`)]}}r(Gs,"moveAsk");const xn=r(e=>h.join(e,"data","checked.json"),"checkedFile");async function Zs(e,t,{idle:i}={}){D(e),i&&t!==void 0&&l("pass --every or --idle, not both"),i===void 0&&t===void 0&&(i=!await $a(e));const n=new Date,a=i?null:t===void 0?Ri.away:/^[1-9]\d{0,3}$/.test(String(t))?Number(t):l("--every takes the minutes between checks, such as 30");return await x(xn(e),i?{at:n.toISOString(),every:null,idle:!0}:{at:n.toISOString(),every:a}),{file:xn(e),every:a,lines:await ka()}}r(Zs,"writeChecked");async function Vs(e,t,{state:i,text:n,url:a}={}){D(e),/^[1-9]\d{0,6}$/.test(String(t))||l(`review record takes the PR number, such as 437, not "${t}"`),["posted","needs"].includes(i)||l("review record needs --state posted or --state needs"),String(n??"").trim()||l("review record needs the review text, as --text or --file"),a!==void 0&&!/^https:\/\/github\.com\/\S+$/.test(a)&&l("--url takes the review's link on GitHub");const s=h.join(e,"data","reviews.json"),o=await k(s,{}),d=((await k(h.join(e,"data","incoming.json"),{}))?.prs||[]).find($=>$.number===Number(t))||o[t]||l(`PR #${t} is not an incoming PR on this board`),{title:u,author:p,url:f,touches:w=[],asked:m=!1,head:g}=d;return o[t]={number:Number(t),title:u,author:p,url:f,touches:w,asked:m,head:g,state:i,text:n,...a?{review_url:a}:{},at:new Date().toISOString()},await x(s,o),{file:s,lines:[]}}r(Vs,"recordReview");async function $a(e){for(const t of await T(e)){const i=await k(t.file,null);if((i?.items||[]).some(n=>!C.includes(n.status)&&Ge(n,ie(e,i)).length))return!0}return!1}r($a,"hasOpenPR");async function ka(){const e=Oi(new Date);if(e.hold)return e.count?[`queued pushes held: ${Ai(e.hold)}`]:[];const t=await ji();return t?[`sent ${t.sent} of ${t.count} queued push${t.count===1?"":"es"}${t.ok?"":"; the rest stay queued"}`]:[]}r(ka,"flushAfterTick");const tt=r(e=>h.join(e,"data","handshake.json"),"handshakeFile"),vn=r(e=>k(tt(e),null),"readHandshake");async function Xs(e,t){D(e),re(t,"the event id");const i=(await W(h.join(e,"data","events.jsonl"))).find(a=>a.id===t);i?.kind!=="hello"&&l(`no hello event "${t}" in this site's data/events.jsonl`);const n=await vn(e)||{};return n.hello?.id&&n.hello.id!==t&&l(`"${t}" is an older hello; ack the newest, "${n.hello.id}"`),await x(tt(e),{...n,hello:n.hello||{id:t,at:i.ts,session:null},ack:{id:t,at:new Date().toISOString()}}),tt(e)}r(Xs,"ackHello");async function Ys(e){const t=await vn(e);t&&!t.done_at&&await x(tt(e),{...t,done_at:new Date().toISOString()})}r(Ys,"finishHandshake");const Dt={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})$/},Ee=r((e,t)=>e.trim().toLowerCase()===t.trim().toLowerCase(),"sameName");function Pt(e,t){return Array.isArray(e)||l(`${t} must be a JSON array of reviewers`),e.forEach((i,n)=>{const a=`${t}[${n}]`;Re(i,Object.keys(Dt),a);for(const[s,o]of Object.entries(Dt))(typeof i[s]!="string"||!o.test(i[s].trim()))&&l(`${a}.${s} must match ${o}`);e.slice(0,n).some(s=>Ee(s.name,i.name))&&l(`${t} names "${i.name}" twice`)}),e.map(i=>Object.fromEntries(Object.keys(Dt).map(n=>[n,i[n].trim()])))}r(Pt,"checkReviewers");async function At(e,t){const{file:i,reviewers:n}=await Lt(e,s=>{const o=Pt(t(s.reviewers||[]),"the reviewers");return{set:{reviewers:o},reviewers:o}}),a=[];for(const s of await T(e)){const o=await k(s.file,null);(o?.items||[]).some(c=>!C.includes(c.status))&&(await x(s.file,{...o,reviewers:n}),a.push(`wave ${s.slug}: reviewers copied`))}return{file:i,lines:a}}r(At,"writeReviewers");const eo=r((e,t)=>At(e,()=>Pt(t,"the reviewer file")),"setReviewers"),_n=r((e,t)=>((typeof e!="string"||!/^\d+(?:\.\d+)?$/.test(e)||Number(e)<=0)&&l("--ts must be a Slack message timestamp"),(typeof t!="string"||!/^D[A-Z0-9]+$/.test(t))&&l("--channel must be a DM channel id"),{ts:e,channel:t}),"linkMessage"),Rt=r((e,t,i)=>xt(e,t).find(n=>n.name===i)||l(`no reviewer "${i}" on this wave or board`),"reviewerOn"),to=r((e,t,i,{ts:n,channel:a})=>_e(e,async()=>{const s=_n(n,a),{source:o,wave:c,item:d}=await P(e,t);return Rt(c,(await Q(e)).reviewers,i),d.review_link_ts={...d.review_link_ts,[i]:s},await x(o.file,c),o.file}),"reviewSent"),no=r((e,t,i,n)=>_e(e,async()=>{(typeof n!="string"||!/^[\w+-]+$/.test(n))&&l("emoji must be a Slack emoji name without colons");const{source:a,wave:s,item:o}=await P(e,t),c=Rt(s,(await Q(e)).reviewers,i),d=yt(s,o,t,bt())[i];d?.ts||l(`no review link recorded for ${i} on ${t}`);const u=M(o.review_link_ts,i),p=typeof u=="object"&&Array.isArray(u?.reactions)?u.reactions:[];return p.includes(n)||(o.review_link_ts={...o.review_link_ts,[i]:{...d,channel:d.channel||c.dm,reactions:[...p,n]}},await x(a.file,s)),a.file}),"reviewReact"),Sn=r(e=>h.join(D(e),"data","review-digests.json"),"digestFile");async function io(e,t){D(e);const i=await ln.waiting("slack");if(i)return{waiting:i,dms:[],reactions:[],digest:null};const n=await Q(e),a=await k(Xt,{}),s=bt(),o=`board.mjs --site ${h.basename(e)}`,c=r(f=>/^[\w.@-]+$/.test(f)?f:JSON.stringify(f),"arg"),d=[],u=[];for(const f of await T(e)){const w=await k(f.file,{}),m=xt(w,n.reviewers),g=ie(e,w);for(const $ of w.items||[]){const{prs:y,pr:v}=jn(a,$,g);if(!v||["done","dropped"].includes($.status))continue;const _=`${f.slug}/${$.id}`,R=yt(w,$,_,s);if($.status==="merged"||$.merged_at||on(y.map(j=>j.gh)))for(const j of m){const O=M(R,j.name),E=M($.review_link_ts,j.name);!O?.ts||Array.isArray(E?.reactions)&&E.reactions.includes("git_merged")||u.push({key:_,reviewer:j.name,channel:O.channel||j.dm,ts:O.ts,emoji:"git_merged",record:`${o} review react ${_} ${c(j.name)} git_merged`})}else if(v.gh?.state==="OPEN"&&!v.gh.isDraft){const j=y.filter(O=>O.gh?.state==="OPEN").map(O=>O.url).join(`
|
|
10
|
+
`);for(const O of m.filter(E=>!M(R,E.name)))d.push({key:_,reviewer:O.name,channel:O.dm,text:j,record:`${o} review sent ${_} ${c(O.name)} --ts <ts> --channel ${O.dm}`})}}}const p=await ya(e,t);return{dms:d,reactions:u,digest:p.due?{...p,record:`${o} review digest sent <reviewer> --ts <ts> --channel <dm> --items <key,key>`}:null}}r(io,"reviewDue");function jn(e,t,i){const n=Ge(t,i).map(a=>({...a,url:Ji(a),gh:M(e.prs,zi(a))||null}));return{prs:n,pr:n[Ki(n.map(a=>a.gh))]||null}}r(jn,"cachedPRs");async function ya(e,{now:t=new Date}={}){D(e);const i=await Q(e),n=await k(Xt,{}),a=bt(),s=new Map;for(const p of await T(e)){const f=await k(p.file,{}),w=xt(f,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=ie(e,f);for(const g of f.items||[]){if(["merged","done","dropped"].includes(g.status))continue;const $=`${p.slug}/${g.id}`,{pr:y}=jn(n,g,m),v=y?.gh;if(v?.state!=="OPEN"||v.isDraft||v.failing?.length||v.reviewDecision==="APPROVED")continue;const _=yt(f,g,$,a);for(const R of w){const j=M(_,R.name),O=j?.ts?(t.getTime()-Number(j.ts)*1e3)/864e5:null;s.get(R.name).items.push({key:$,title:g.title,pr:y.number,url:y.url,sent_ts:j?.ts||null,...O>2?{days_old:Math.floor(O)}:{}})}}}const{date:o,morning:c}=dn(t),d=await k(Sn(e),{}),u=await ln.waiting("slack");return{date:o,due:!u&&c&&d.last_date!==o,...u?{waiting:u}:{},reviewers:[...s.values()].filter(p=>p.items.length)}}r(ya,"reviewDigest");async function ao(e,t,{ts:i,channel:n,items:a},{now:s=new Date}={}){const o=_n(i,n),c=[...new Set(S(a,"--items").split(",").map(p=>p.trim()))],d=await Q(e);for(const p of c){const{wave:f}=await P(e,p);Rt(f,d.reviewers,t)}const u=Sn(e);return kt(u,async()=>{const p=await k(u,{});return await x(u,{last_date:dn(s).date,digests:[...p.digests||[],{reviewer:t,dm:o.channel,ts:o.ts,items:c,reactions:[]}]}),u})}r(ao,"reviewDigestSent");function so(e,t){const i=Pt(Array.isArray(t)?t:[t],"the reviewer file");return At(e,n=>[...n.map(a=>i.find(s=>Ee(s.name,a.name))||a),...i.filter(a=>!n.some(s=>Ee(a.name,s.name)))])}r(so,"addReviewers");function oo(e,t){return At(e,i=>{const n=t.filter(a=>!i.some(s=>Ee(s.name,a)));return n.length&&l(`this board has no reviewer ${n.map(a=>`"${a}"`).join(", ")}`),i.filter(a=>!t.some(s=>Ee(a.name,s)))})}r(oo,"removeReviewers");const ba=r(()=>h.join(ft.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),Nt=r(e=>{try{return e()}catch(t){throw t instanceof Qi&&l(t.message),t}},"ruleCall"),Et=r(async e=>un(await k(h.join(e,"board.json"),{})),"readBoardRules");let On=Promise.resolve();function Lt(e,t){const i=On.then(async()=>{const n=h.join(D(e),"board.json"),a=await k(n,{}),{set:s,...o}=t(a);return await x(n,{...a,...s}),{file:n,...o}});return On=i.catch(()=>{}),i}r(Lt,"writeBoardJSON");const Dn=r((e,t)=>Lt(e,i=>{const n=Nt(()=>t(un(i)));return{set:{rules:n.rules},...n}}),"writeBoardRules"),Pn=r(e=>typeof e=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e),"validGithubLogin"),An=r(e=>{try{return e?gt(e):null}catch{return null}},"storedTickets"),Rn=r(e=>Number.isInteger(e)&&e>=1&&e<=99,"validMaxImplementers"),me=r(e=>{const t=An(e?.tickets);return{auto_merge:e?.auto_merge===!0,auto_build:e?.auto_build===!0,auto_pull:e?.auto_pull===!0,max_implementers:Rn(e?.max_implementers)?e.max_implementers:ki,auto_triage:e?.auto_triage===!0,auto_review:e?.auto_review===!0,headless_builders:e?.headless_builders===!0,idle_notification:e?.idle_notification===!0,skip_draft:e?.skip_draft===!0,...t?{tickets:t}:{},...Pn(e?.github_login)?{github_login:e.github_login}:{}}},"settingsOf");async function Nn(e){return me(await k(h.join(D(e),"board.json"),{}))}r(Nn,"readBoardSettings");function Le(e,t){Re(t,["auto_merge","auto_build","auto_pull","max_implementers","auto_triage","auto_review","headless_builders","idle_notification","skip_draft","tickets","github_login"],"the settings"),Object.keys(t).length||l("the settings name nothing to change"),t={...t};for(const[i,n]of Object.entries(t))if(i==="tickets"){try{t.tickets=gt(n)}catch(s){l(s.message)}t.tickets.source==="linear"&&!Fi()&&l(Ti);const a=t.tickets.source==="github"&&e?ke().find(s=>Pe(s.dir,e)):null;a&&!a.github&&l("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:Pn(n)||l("github_login must be a GitHub login"):i==="max_implementers"?Rn(n)||l("max_implementers must be a whole number from 1 to 99"):_t(n,i);return Lt(e,i=>{const n=me(i),a=me({...i,...t});return{set:t,settings:a,changed:Object.keys(t).filter(s=>JSON.stringify(n[s])!==JSON.stringify(a[s]))}})}r(Le,"saveBoardSettings");async function ro(){if(qi().set)return[];const e=[];for(const t of ke()){if((await Nn(t.dir)).tickets?.source!=="linear")continue;const{settings:i}=await Le(t.dir,{tickets:{source:"internal"}});await De(t,"settings",{tickets:i.tickets}),e.push(`/${t.route} fell back to internal tickets: Hub has no Linear key`)}return e}r(ro,"fallBackFromLinear");async function co(e,t,i){const n=String(t).replace(/-/g,"_"),a=["auto_merge","auto_build","auto_pull","max_implementers","auto_triage","auto_review","headless_builders","idle_notification","skip_draft","github_login","tickets"];if(a.includes(n)||l(`no board setting "${t}"; settings are ${a.map(c=>c.replace(/_/g,"-")).join(", ")}`),n==="tickets"){let c;try{c=JSON.parse(i)}catch{l('settings set tickets takes the source as JSON, such as {"source":"github"}')}const{file:d,settings:u}=await Le(e,{tickets:c});return{file:d,lines:xa({tickets:u.tickets})}}if(n==="github_login"){const{file:c,settings:d}=await Le(e,{github_login:i});return{file:c,lines:[`github-login ${d.github_login||"unset"}`]}}if(n==="max_implementers"){const{file:c,settings:d}=await Le(e,{max_implementers:/^\d+$/.test(i)?Number(i):i});return{file:c,lines:[`${t} ${d.max_implementers}`]}}["on","off"].includes(i)||l(`settings set ${t} takes on or off, not "${i}"`);const{file:s,settings:o}=await Le(e,{[n]:i==="on"});return{file:s,lines:[`${t} ${o[n]?"on":"off"}`]}}r(co,"setBoardSetting");const xa=r(e=>Object.entries(e).map(([t,i])=>`setting ${t.replace(/_/g,"-")} ${t==="tickets"?JSON.stringify(i):t==="github_login"||t==="max_implementers"?i:i?"on":"off"}`),"settingsLines");async function It(e,t,i){const n=t?Nt(()=>i(Ve())):await Dn(e,i);return t&&Nt(()=>Yi(n.rules)),{file:t?ba():n.file,lines:[`id ${n.rule.id}`]}}r(It,"changeRules");async function En(e,t,i){const n=r(o=>o.map(c=>c.id),"ids");i||D(e);const[a,s]=i?[n(Ve()),e?n(await Et(e)):[]]:[n(await Et(e)),n(Ve())];a.includes(t)||l(`no ${i?"hub":"board"} rule "${t}"${s.includes(t)?`; it is a ${i?"board":"hub"} rule, so ${i?"drop":"pass"} --hub`:""}`)}r(En,"checkRuleId");async function lo(e){const t=r(i=>n=>`${i} ${n.id} ${n.by==="claude"?"claude":"owner"} ${n.text}`,"line");return{lines:[...Ve().map(t("hub")),...e?(await Et(e)).map(t("board")):[]]}}r(lo,"listRules");function uo(e,{text:t,hub:i=!1,by:n}){return i||D(e),It(e,i,a=>Gi(a,{text:t,by:n}))}r(uo,"addRuleTo");async function fo(e,t,{text:i,hub:n=!1}){return await En(e,t,n),It(e,n,a=>Zi(a,t,i))}r(fo,"editRuleIn");async function wo(e,t,{hub:i=!1}){return await En(e,t,i),It(e,i,n=>Vi(n,t))}r(wo,"removeRuleFrom");const po=r((e,t,i)=>Dn(e,n=>Xi(n,t,i)),"saveBoardRules");async function mo(e,{id:t,title:i,summary:n}){D(e);const a=await k(L(e),{topics:[],items:[]}),s=a.topics||[];return s.some(o=>o.id===S(t,"the topic id"))&&l(`topic "${t}" already exists`),s.push({id:t.trim(),title:S(i,"--title"),...n===void 0?{}:{summary:S(n,"--summary")}}),await x(L(e),{...a,topics:s,items:a.items||[]}),L(e)}r(mo,"addTopic");const Ln=["quiet","info","notice","danger","success"];function va(e,t){if(e.diagram===void 0!=(e.practice===void 0)&&l(`${t} needs diagram and practice together`),e.diagram===void 0)return;const i=e.diagram;(!Array.isArray(i)||!i.length)&&l(`${t}.diagram must be a non-empty array of rows`),i.forEach((n,a)=>{(!Array.isArray(n)||!n.length||n.length>4)&&l(`${t}.diagram[${a}] must be a row of 1 to 4 boxes`),n.forEach((s,o)=>{const c=`${t}.diagram[${a}][${o}]`;Re(s,["text","tone","to"],c),S(s.text,`${c}.text`),Z(s.tone,d=>Ln.includes(d)?d:l(`${c}.tone takes ${Ln.join(", ")}`)),Z(s.to,d=>Array.isArray(d)&&a+1<i.length&&d.every(u=>Number.isInteger(u)&&u>=0&&u<i[a+1].length)?d:l(`${c}.to must list boxes of the next row by index`))})}),(!Array.isArray(e.practice)||!e.practice.length)&&l(`${t}.practice must be a non-empty array of lines`),e.practice.forEach((n,a)=>S(n,`${t}.practice[${a}]`))}r(va,"checkDiagram");function _a(e,t){(!Array.isArray(e)||!e.length)&&l(`${t}.options must be a non-empty array`);const i=new Set;e.forEach((n,a)=>{const s=`${t}.options[${a}]`;Re(n,["id","label","recommended","detail","diagram","practice"],s),va(n,s),i.has(S(n.id,`${s}.id`))&&l(`${t} has two options with id "${n.id}"`),i.add(n.id),S(n.label,`${s}.label`).startsWith(`${n.id}. `)&&l(`${s}.label starts with "${n.id}. ", but the page adds the option letter itself`),Z(n.recommended,o=>_t(o,`${s}.recommended`)),Z(n.detail,o=>typeof o=="string"?o:l(`${s}.detail must be text`))}),e.filter(n=>n.recommended).length>1&&l(`${t} recommends more than one option`)}r(_a,"checkOptions");const Ie=r(e=>h.join(e,"data","prototypes.json"),"prototypesFile"),Bt=r((e,t,i)=>h.join(e,"public","prototypes",t,`${i}.js`),"takeFile");async function nt(e){const t=e?(await k(Ie(e),{})).prototypes:null;return Array.isArray(t)?t:[]}r(nt,"readPrototypes");const Sa=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:l(`a prototype topic uses only a-z, 0-9 and "-", not "${e}"`),"prototypeTopic");function ja(e){const t=/^([^:]*):(.*)$/s.exec(e);(!t||!/^[A-Z]$/.test(t[1]))&&l(`--take takes KEY:"Label" with KEY one capital letter, not "${e}"`);const i=S(t[2],`--take ${t[1]}'s label`);return/^[A-Z]\.\s/.test(i)&&l(`--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(ja,"prototypeTake");const xe=r(e=>/^prototype-(.+)\.html$/.exec(h.posix.basename(String(e||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function ho(e,{topic:t,title:i,card:n,takes:a=[]}){D(e),Sa(t),S(i,"--title"),a.length||l('prototype add needs a --take KEY:"Label" for each take');const s=a.map(ja),o=s.find((u,p)=>s.findIndex(f=>f.key===u.key)!==p);o&&l(`--take ${o.key} is given twice`),n!==void 0&&await P(e,n);const c=await nt(e);c.some(u=>u.topic===t)&&l(`prototype ${t} is already recorded; run prototype rm ${t} first, or pick another topic`),await A.stat(h.join(e,"public",`prototype-${t}.html`)).then(()=>!0,()=>!1)&&l(`public/prototype-${t}.html is a hand-written page that questions may link; pick another topic`);const d={topic:t,title:i.trim(),...n===void 0?{}:{card:n},takes:s,at:new Date().toISOString()};return await x(Ie(e),{prototypes:[...c,d]}),{file:Ie(e),lines:[`page prototype-${t}.html`,...s.map(u=>`take ${u.key} ${Bt(e,t,u.key)}`)]}}r(ho,"addPrototype");async function go(e){D(e);const t=await nt(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(go,"listPrototypes");async function $o(e,t){D(e);const i=await nt(e);i.some(s=>s.topic===t)||l(`no prototype "${t}" is recorded`);const a=((await k(L(e),{items:[]})).items||[]).filter(s=>!s.resolved&&xe(s.url)===t).map(s=>s.id);return a.length&&l(`prototype ${t} backs open question ${a.join(", ")}; resolve ${a.length===1?"it":"them"} first`),await x(Ie(e),{prototypes:i.filter(s=>s.topic!==t)}),Ie(e)}r($o,"removePrototype");async function Oa(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&&l(`${o} must be ${a}?q=${t.id} so the pill can pick`);const c=xe(n),d=(await nt(e)).find(w=>w.topic===c);d||l(`${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 u=d.takes.map(w=>w.key),p=t.options.map(w=>w.id),f=p.map(w=>u.indexOf(w));f.some((w,m)=>w<0||m>0&&w<f[m-1])&&l(`${i}.options ids ${p.join(", ")} must be a non-empty subset of ${a}'s takes, in take order: ${u.join(", ")}`);for(const w of u)await A.stat(Bt(e,c,w)).then(m=>m.isFile(),()=>!1)||l(`${o} take ${w} has no file yet; write ${Bt(e,c,w)}`)}r(Oa,"checkPrototype");async function ko(e,t){D(e);const i=Array.isArray(t)?t:[t];i.length||l("the question file is empty");const n=await k(L(e),{topics:[],items:[]}),a=n.items||[],s=new Set(a.map(w=>w.id)),o=new Set((n.topics||[]).map(w=>w.id));for(const[w,m]of i.entries()){const g=`question ${m?.id??w+1}`;Re(m,["id","topic","source","question","context","url","urgent","options","round_of","ask"],g),s.has(re(m.id,`${g}.id`))&&l(`question id "${m.id}" is already used`),s.add(m.id),S(m.topic,`${g}.topic`),m.topic.startsWith("item:ask:")?await Be(e,m.topic.slice(9),`${g}.topic`):m.topic.startsWith("item:")?await P(e,m.topic.slice(5)):m.topic.startsWith("lineup:")?await lt(e,m.topic.slice(7)):m.topic.startsWith("wave:")?await Qa(e,m.topic.slice(5)):o.has(m.topic)||l(`topic "${m.topic}" is not declared; add it with: topic add ${m.topic} --title <t>`),S(m.question,`${g}.question`);for(const $ of["source","context"])Z(m[$],y=>S(y,`${g}.${$}`));Z(m.url,$=>fn($,`${g}.url`)),Z(m.urgent,$=>_t($,`${g}.urgent`)),_a(m.options,g),await Oa(e,m,g),m.ask!==void 0&&await ss(e,m,g),Z(m.round_of,$=>{const y=a.find(v=>v.id===$);y||l(`${g}.round_of names no question "${$}" in owner.json`),y.topic!==m.topic&&l(`${g}.round_of names ${$} on topic ${y.topic}; a round stays on its question's topic, ${m.topic}`),xe(m.url)||l(`${g}.round_of needs a url on a recorded prototype, prototype-<topic>.html?q=${m.id}`)})}const c=await it(e),d=new Date().toISOString(),u=[],p=[];for(const w of i.filter(m=>m.round_of))w.round=ht(a.find(m=>m.id===w.round_of))+1,ve(n,[w.round_of],`Followed by prototype round ${w.round}`,d),p.push(w.round_of);for(const w of i){const m=xe(w.url),g=a.filter(y=>y.topic===w.topic).map(y=>Oe(y,c)),$=w.topic.startsWith("item:")?In(g):[];for(const y of g)$.includes(y.id)?(ve(n,[y.id],`Followed by ${w.id}`,d),p.push(y.id)):!y.resolved&&m&&xe(y.url)===m&&(ve(n,[y.id],`Replaced by ${w.id}`,d),u.push(y.id))}const f={...n,topics:n.topics||[],items:[...a,...i.map(w=>({...w,asked_at:d}))]};return await x(L(e),f),{file:L(e),lines:[...u.map(w=>`replaced: ${w}`),...p.map(w=>`resolved: ${w}`)]}}r(ko,"addQuestions");function In(e){return e.filter(t=>t.state==="answered").map(t=>t.id)}r(In,"answeredOnClaude");function ve(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(ve,"setResolved");async function Ct(e,t,i,n){return ve(t,i,n),await x(L(e),t),L(e)}r(Ct,"markResolved");async function yo(e,t,i){D(e);const n=await k(L(e),{topics:[],items:[]}),a=(n.items||[]).find(u=>u.id===t);a||l(`no question "${t}" in owner.json`);const s=await it(e);if(Oe(a,s).state==="resolved")return L(e);const c=S(i,"--note");xe(a.url)&&Da(n,a,s);const d=await Ct(e,n,[t],c);return await Fn(e,a.topic),d}r(yo,"resolveQuestion");async function bo(e,t){const i=t.filter(({choice:a,note:s})=>!a&&String(s||"").trim()).map(a=>a.id),n=i.length?await k(L(e),{topics:[],items:[]}):null;n?.items?.some(a=>i.includes(a.id))&&(ve(n,i,"Answered with a note"),await x(L(e),n))}r(bo,"resolveNoteAnswers");async function xo(e,t){const i=await k(L(e),{topics:[],items:[]});i.items?.some(n=>n.id===t)&&(ve(i,[t],"Prototype requested"),await x(L(e),i))}r(xo,"resolveRequestedQuestion");function Da(e,t,i){const n=ht(t),a=e.items.filter(s=>s.topic===t.topic&&ht(s)===n).map(s=>Oe(s,i));_i(a,n)||l(`${t.id} is in prototype round ${n}, which no pick has settled yet; a pick settles it`)}r(Da,"roundSettled");async function it(e){const t=(await W(h.join(e,"data","events.jsonl"))).filter(i=>i.kind==="owner-answer");return Promise.all(t.map(async i=>{const n=await Promise.all(["html","json"].map(a=>A.stat(h.join(e,"data","replies",`${i.id}.${a}`)).then(s=>s.isFile(),()=>!1)));return{...i,reply:i.reply||n.some(Boolean)}}))}r(it,"ownerAnswers");async function Bn(e,t){const i=await k(L(e),{topics:[],items:[]}),n=await it(e),a=(i.items||[]).filter(s=>s.topic===t).map(s=>Oe(s,n));return{owner:i,asked:a}}r(Bn,"topicQuestions");const le=r((e,t)=>Bn(e,`item:${t}`),"cardQuestions"),Tt=r((e,t,i)=>Cn(e,`item:${t}`,i),"resolveAnswered");async function Cn(e,t,i){if(!e)return[];const{owner:n,asked:a}=await Bn(e,t),s=In(a);return s.length&&await Ct(e,n,s,i),await Fn(e,t),s.map(o=>`resolved: ${o}`)}r(Cn,"resolveAnsweredOn");const Tn=r(async(e,t)=>!!e&&(await le(e,t)).asked.some(i=>i.state==="open"),"hasOpenQuestion");function qn(e,t,i){e.next=S(t,"--next"),i?e.next_on_questions=!0:delete e.next_on_questions}r(qn,"setNext");async function Fn(e,t){const i=t.startsWith("item:")&&!t.startsWith("item:ask:")?t.slice(5):null;if(!i)return;let n;try{n=await P(e,i)}catch(c){if(c instanceof te)return;throw c}const{source:a,wave:s,item:o}=n;o.next_on_questions&&((await le(e,i)).asked.some(c=>c.state!=="resolved")||(delete o.next,delete o.next_on_questions,await x(a.file,s)))}r(Fn,"clearWaitedNext");async function Pa(e,t){const{source:i,wave:n,item:a}=await P(e,t);a.next===void 0&&a.next_on_questions===void 0||(delete a.next,delete a.next_on_questions,await x(i.file,n))}r(Pa,"clearNext");async function Mn(e,t,i,n){if(!e)return async()=>[];const{owner:a,asked:s}=await le(e,t),o=s.filter(d=>d.state==="open").map(d=>d.id);o.length&&!n&&l(`${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 Ct(e,a,c,"Closed with the card"),c.map(d=>`resolved: ${d}`))}r(Mn,"closeQuestions");async function vo(e,t,i,n){await P(e,t),S(i,"--text"),Z(n,a=>S(a,"--owner-step")),await Mn(D(e),t,"land",!1)}r(vo,"checkClose");async function Un(e,t){const i=await k(L(e),{items:[]}),n=await it(e);return bi((i.items||[]).map(a=>Oe(a,n)),t)}r(Un,"unpickedPrototype");const Aa=r(e=>e.length>120?`${e.slice(0,119)}\u2026`:e,"clip"),Ra={ask:"text","ask-edited":"text","item-message":"text","owner-answer":"note"};async function Wn(e,t,i){const n=S(t,i).replace(/\s+/g," ");return(await W(h.join(e,"data","events.jsonl"))).map(s=>String(s.data?.[Ra[s.kind]]??"").replace(/\s+/g," ").toLowerCase()).some(s=>s.includes(n.toLowerCase()))||l(`${i} must quote the owner's words in an ask, card message or answer note on this board`),n}r(Wn,"ownerQuote");async function _o(e,t,i){return D(e),i===void 0&&l(`only the owner's Serve / Build and the hub's Auto-build start a card run; pass --asked "<the owner's words>" quoting where the owner asked for ${t}'s run`),`Running at the owner's ask: "${await Wn(e,i,"--asked")}"`}r(_o,"askedGate");async function Be(e,t,i="--from-ask"){return re(t,i),(await W(h.join(D(e),"data","events.jsonl"))).some(a=>a.id===t&&a.kind==="ask")?t:l(`no ask "${t}" in this site's data/events.jsonl`)}r(Be,"checkAsk");const C=["done","merged","dropped"],Na=["ready","grilling","prototyping"];function qt(e){return["building","fixing"].includes(e?.status)?(delete e.status,e.status_at=new Date().toISOString(),!0):!1}r(qt,"clearBuildingStatus");function Ea(e,t,i){const n=[].concat(t).map(o=>rn(o)||l(`--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=cn(e);for(const o of n)s.some(c=>a(c,o))||s.push(o);delete e.pr,e.prs=s}r(Ea,"addPRs");const V=r((e,t)=>String(t).includes("/")?String(t):`${e}/${t}`,"depKey");async function at(e,t,i){const n=(i.deps||[]).map(s=>V(t,s)),a=await Promise.all(n.map(s=>P(e,s).then(({item:o})=>o,()=>null)));return n.filter((s,o)=>a[o]&&!C.includes(a[o].status))}r(at,"unfinishedDeps");async function La(e,t){const i=[];for(const n of await T(e)){const a=await k(n.file,null);for(const s of Array.isArray(a?.items)?a.items:[])![void 0,"planned","waiting","held"].includes(s.status)||!(s.deps||[]).some(o=>V(n.slug,o)===t)||(await at(e,n.slug,s)).length||i.push({key:`${n.slug}/${s.id}`,queued:!!s.queued_at})}return i}r(La,"unblockedBy");async function Hn(e,t,i,n){const a=new Set(n.match(/[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),s=new Set((i.deps||[]).map(o=>V(B(t).wave,o)));for(const o of await T(e)){const c=await k(o.file,null);for(const d of c?.items||[]){const u=`${o.slug}/${d.id}`;if(u===t||s.has(u)||C.includes(d.status))continue;[d.id,Qe(c,d)].flatMap(f=>[f,`${o.slug}/${f}`]).some(f=>a.has(f))&&l(`--next names unfinished card ${u}; add it with --deps`)}}}r(Hn,"checkNextDeps");async function Ia(e,t,i,n){const a="a headless builder starts from it, so pass";n.size||l(`${t} has no size; ${a} --size small or large with --state ready`),n.plan||l(`${t} has no notes file; ${a} --notes-file <file.md> with --state ready`);const s=await A.readFile(await Ce(i,n.plan),"utf8"),o=(await le(e,t)).asked.filter(c=>c.answers.length&&!s.includes(c.id)).map(c=>c.id);o.length&&l(`${t}'s notes leave out the answered question${o.length===1?"":"s"} ${o.join(", ")}; record each answer in ${n.plan}, naming its id, before it is ready`)}r(Ia,"checkNotesComplete");async function Ce(e,t){const i=r(a=>l(`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 A.realpath(h.resolve(e,t)).catch(()=>i("does not exist"));return n.startsWith(await A.realpath(e)+h.sep)||i("leaves the folder"),n}r(Ce,"planPath");async function So(e,t){const{source:i,wave:n,item:a}=await P(e,t);if(a.plan)return{wave:n,item:a,markdown:await A.readFile(await Ce(i.dir,a.plan),"utf8")};if(typeof a.notes=="string"&&a.notes.trim())return{wave:n,item:a,markdown:a.notes};l(`${t} has no notes`)}r(So,"readNotes");function Kn({install:e,installSha:t,installLabel:i,clearInstall:n}){if(e===void 0)return(t!==void 0||i!==void 0)&&l("--install-sha and --install-label go with --install <url>"),n?null:void 0;n&&l("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))&&l(`--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")||""))&&l("--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():l(`--install-sha takes a commit sha of 7 to 40 hex digits, not "${t}"`);return{url:String(e).trim(),...i===void 0?{}:{label:S(i,"--install-label")},built_at:new Date().toISOString(),...s?{sha:s}:{}}}r(Kn,"installOf");async function jo(e,{install:t,installSha:i}){const n=Kn({install:t,installSha:i});n||l("main set needs --install <url>");const a=h.join(D(e),"data","main.json");return await x(a,{install:n}),{file:a,lines:["main install set"]}}r(jo,"setMainInstall");const zn=r((e,t)=>e.length?`Merged PR${e.length>1?"s":""} ${Ae(e,t)}.`:"Merged.","mergedLine");function Ba(e,t,i,n,a,s){return n==="merged"?a??(i.status==="merged"?null:zn(cn(i),s)):(a===void 0&&e&&l(`--status ${n} needs --text "<what was done or left>", which becomes ${t}'s last thread line`),a??null)}r(Ba,"closingLine");async function Jn(e,t,{ticket:i,status:n,clearStatus:a,next:s,clearNext:o,pr:c,pushed:d,ghSince:u,fromAsk:p,state:f,held:w,unheld:m,pickUpAt:g,notesFile:$,deps:y,parent:v,related:_,install:R,installSha:j,installLabel:O,clearInstall:E,title:F,size:se,model:K,force:U,text:q,who:Fe,ownerStep:X,ownerDone:je},oe){n!==void 0&&a&&l("pass --status or --clear-status, not both"),f!==void 0&&!Na.includes(f)&&l("--state takes ready, grilling or prototyping"),U&&!C.includes(n)&&l(`--force goes with --status ${C.join(", ")}`),p!==void 0&&C.includes(n)&&l("--from-ask opens a card; close it in another call"),q!==void 0&&S(q,"--text"),X!==void 0&&S(X,"--owner-step"),q!==void 0&&p===void 0&&!C.includes(n)&&l(`--text goes with --from-ask or --status ${C.join(", ")}`),p!==void 0&&q===void 0&&l(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),s!==void 0&&o&&l("pass --next or --clear-next, not both"),typeof s=="string"&&!s.trim()&&([s,o]=[void 0,!0]);const ge=Ca({held:w,unheld:m,pickUpAt:g}),$e=Kn({install:R,installSha:j,installLabel:O,clearInstall:E});[n,a,s,o,c,d,u,p,f,ge,$,y,v,_,$e,F,se,K,i,X].every(N=>N===void 0||N===!1)&&l("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, --install, --clear-install, --title, --size, --model or --ticket");const I=p===void 0?void 0:await Be(e,p);I&&await Ot(e,I);const{source:z,wave:de,item:b}=await P(e,t);I&&b.from_ask&&b.from_ask!==I&&l(`${t} came from ask "${b.from_ask}", so ask "${I}" can't be its from-ask; to fold it in: item relate-ask ${I} ${t} --summary "<line>"`),I&&f===void 0&&!b.state&&l("--from-ask files a card, which reads New until it has a state: pass --state ready, grilling or prototyping");const Me=b.status,Ue=C.includes(b.status);X!==void 0&&Ue&&l(`${t} is already ${b.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`),X!==void 0&&(b.owner_step=S(X,"--owner-step")),(je||n==="dropped")&&delete b.owner_step;const J=await Promise.all((await ae(z.dir,b.id)).map(N=>k(N,null))),dt=mt(b,J);if($!==void 0&&(await Ce(z.dir,$),b.plan=h.normalize($)),F!==void 0&&(b.title=S(F,"--title")),Qn(b,{size:se,model:K}),y!==void 0){const N=await Zn(e,B(t).wave,y);N.length?b.deps=N:delete b.deps}if(v!==void 0){const N=await Vn(e,B(t).wave,t,v);N?b.parent=N:delete b.parent}const Y=_===void 0?null:await Ma(e,t,b,_);if(ge!==void 0&&st(b,ge),n==="building"&&(b.held||b.status==="held")&&l(b.pick_up_at?`${t} is held until ${b.pick_up_at}, when the board resumes it; start it early only with item set ${t} --unheld`:`${t} is held; start it only once the owner resumes it, with item set ${t} --unheld`),n!==void 0&&(b.status=Vt.includes(n)?n:l(`--status takes ${Vt.join(", ")}`)),(Ta.includes(n)||c!==void 0||d!==void 0)&&qa(b),n==="building"){const N=await at(e,B(t).wave,b);N.length&&l(`${t} is blocked by ${N.join(", ")}; start it once ${N.length===1?"that is":"those are"} done or merged`)}(n==="building"||C.includes(n))&&delete b.queued_at;const We=n==="building"&&e?await Un(e,t):null;We&&l(`${t} has no take picked: the owner's answer to ${We.id} picks none${We.note?` ("${Aa(We.note)}")`:""}, so it prototypes again: draw the next round from that note, or grill when the note asks a question, and build once a take is picked`);const pi=C.includes(n)?await Mn(e,t,`--status ${n}`,U):async()=>[],Kt=C.includes(n)?Ba(e,t,{...b,status:Me},n,q,ie(e,de)):null;if(a&&delete b.status,(n!==void 0||a)&&(b.status_at=new Date().toISOString()),s!==void 0&&(qn(b,s,await Tn(e,t)),await Hn(e,t,b,b.next)),o&&(delete b.next,delete b.next_on_questions),n==="building"&&s===void 0&&b.next_on_questions&&(delete b.next,delete b.next_on_questions),c!==void 0&&(Ea(b,c,ie(e,de)),qt(b)),d!==void 0){ke().find(N=>Pe(N.dir,D(e)))?.remote||l("--pushed is for a board whose repo is a plain git remote; on GitHub, open a PR and pass --pr"),Ze(b)&&l(`${t} has a PR; its PR says when it merges`);try{Mi(d)}catch{l("--pushed takes the name of the branch you pushed")}b.pushed={branch:d,at:new Date().toISOString()},b.branch=d,qt(b)}if(u!==void 0&&((typeof u!="string"||!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(u)||Number.isNaN(Date.parse(u)))&&l("--gh-since takes an ISO-8601 UTC time such as 2026-10-23T11:00:00Z"),b.gh_since=u),I&&(b.from_ask=I),f==="ready"&&e){const N=(await le(e,t)).asked.filter(fe=>fe.state==="open").map(fe=>fe.id);N.length&&l(`${t} has ${N.length===1?"an open question":"open questions"} ${N.join(", ")}; it is ready once the owner has answered ${N.length===1?"it":"them"}`),(await Nn(e)).headless_builders&&await Ia(e,t,z.dir,b)}f!==void 0&&(b.state=f),$e&&(b.install=$e),$e===null&&delete b.install,!dt&&!["building","fixing"].includes(n)&&mt(b,J)&&l(`that leaves ${t} In Progress with nothing building and no Next; pass --next "<what happens next>"`),(i!==void 0||I&&!b.ticket&&!b.linear?.length)&&await Ft(e,t,de,b,i,oe,z.dir),await x(z.file,de),Y&&await Y();const ue=await pi();$!==void 0&&ue.push(...await Tt(e,t,"Recorded in the card's notes")),n==="building"&&ue.push(...await Tt(e,t,"Acted on: build started")),I&&ue.push(...(await Ne(e,t,{text:q})).lines),Kt&&e&&ue.push(...(await Ne(e,t,{text:Kt,who:Fe})).lines);const zt=b.owner_step&&C.includes(n)?null:Li({status:n,pr:c??d}),Jt=zt&&await Ii(await Xn(e),b,zt,{...oe,repo:ie(e,de)});Jt&&ue.push(Jt),(c!==void 0||d!==void 0||os.includes(n))&&await rs(z.dir,b.id,C.includes(n));const ut=I?await pe(e):{};if(M(ut,I)!==void 0&&(delete ut[I],await x(ce(e),{links:ut})),!C.includes(n)||Ue)return{file:z.file,lines:ue};const Qt=await La(e,t),mi=new Date().toISOString();for(const{key:N,queued:fe}of Qt)await ee(H(e,N),{ts:mi,who:"claude",text:`Unblocked: ${await ne(e,t)} is ${n}.`}),fe||await De({dir:e},"card-unblocked",{key:N});return{file:z.file,lines:[...Qt.map(({key:N,queued:fe})=>fe?`queued build starts: ${N}`:`unblocked: ${N}`),...ue]}}r(Jn,"changeItem");function Qn(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:l(`--${n} takes ${s.join(" or ")}, or "" to clear it`))}r(Qn,"setBuildChoice");function Ca({held:e,unheld:t,pickUpAt:i}){return[e,t,i!==void 0].filter(Boolean).length>1&&l("pass one of --held, --unheld and --pick-up-at"),i!==void 0?i===""?null:{until:Gn(i)}:e?{until:null}:t?null:void 0}r(Ca,"holdOf");function Gn(e){const t=typeof e=="string"&&Ni.test(e)?Date.parse(e):NaN;return Number.isNaN(t)&&l('--pick-up-at takes an ISO time such as 2026-11-04T09:00, the Mac\u2019s local time unless it names an offset, or "" to resume the card'),t<=Date.now()&&l(`--pick-up-at ${e} has passed; pass a later time, or --unheld to resume the card now`),new Date(t).toISOString()}r(Gn,"pickUpTime");const Ta=["building","fixing","checking",...C];function qa(e){delete e.held,delete e.pick_up_at}r(qa,"clearHold");function st(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(st,"setHold");async function Oo(e,t,i,{announce:n=!0}={}){const{source:a,wave:s,item:o}=await P(e,t);if(n){const c=new Date(i).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});await ee(H(e,t),{ts:new Date().toISOString(),who:"claude",text:`Resumed: its hold until ${c} ended.`}),!o.queued_at&&!(await at(e,B(t).wave,o)).length&&await De({dir:e},"card-unblocked",{key:t,until:i})}st(o,null),await x(a.file,s)}r(Oo,"pickUp");async function Fa(e,t,i){const n=String(i?.key||""),{wave:a,id:s}=B(n),{source:o,wave:c}=await ye(e,a),d={text:"Marked done.",who:"love"},u=c.items.find(m=>m.id===s)||l(`wave "${a}" has no item "${s}"`);if(!(t==="card-done"&&!!u.owner_step)&&["merged","dropped"].includes(u.status))return null;if(t==="card-done")return Se(e,n,{status:u.status==="merged"?"merged":"done",force:!0,ownerDone:!0,...d});const f=i.hold?typeof i.until=="string"?new Date(i.until).toISOString():null:void 0;st(u,i.hold?{until:f}:null),await x(o.file,c);const w=f&&new Date(f).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});return Ne(e,n,{text:i.hold?w?`Held until ${w}.`:"Held.":"Resumed.",who:"love"})}r(Fa,"recordOwnerTap");async function Zn(e,t,i){const n=[...new Set(String(i).split(",").map(a=>a.trim()).filter(Boolean))];for(const a of n)await P(e,V(t,a));return n}r(Zn,"depList");async function Vn(e,t,i,n){if(!String(n).trim())return null;const a=V(t,String(n).trim());a===i&&l("--parent: a card can't be its own Parent");for(let s=a;s;){const{item:o}=await P(e,s);o.parent&&V(B(s).wave,o.parent)===i&&l(`--parent: ${a} is a child of ${i}, so it can't be its Parent`),s=o.parent?V(B(s).wave,o.parent):null}return a}r(Vn,"parentKey");async function Ma(e,t,i,n){const a=[...new Set(String(n).split(",").map(s=>s.trim()).filter(Boolean).map(s=>V(B(t).wave,s)))];a.includes(t)&&l("--related: a card can't be related to itself");for(const s of a)await P(e,s);return a.length?i.related=a:delete i.related,async()=>{for(const s of await T(e)){const o=await k(s.file,null);let c=!1;for(const d of Array.isArray(o?.items)?o.items:[]){const u=`${s.slug}/${d.id}`;u===t||a.includes(u)||!(d.related||[]).some(p=>V(s.slug,p)===t)||(d.related=d.related.filter(p=>V(s.slug,p)!==t),d.related.length||delete d.related,c=!0)}c&&await x(s.file,o)}}}r(Ma,"setRelated");const ie=r((e,t)=>e&&ke().find(i=>Pe(i.dir,e))?.github||t.repo,"boardRepoOf");async function Do(e,t){const{wave:i,item:n}=await P(e,t);Ze(n)||l(`${t} has no PR`);const a=Ge(n,ie(e,i));a.length||l(`${t} has no repo`);const s=n.gh_since||"1970-01-01T00:00:00Z";try{const o=me(await Q(e)).github_login||await Ui();return await Promise.all(a.map(async({repo:c,number:d})=>({repo:c,...an(await nn({repo:c,pr:d,since:s}),{pr:d,since:s,me:o})})))}catch(o){l(o.message)}}r(Do,"prReport");async function Po(e,t,{replyTo:i,body:n}){const a=i===void 0?"pr comment":"pr reply",s=rn(t)||l(`${a} takes a PR number or owner/repo#<n>`);i!==void 0&&!/^[1-9]\d*$/.test(i)&&l("pr reply takes the id of the review comment it answers");const o=S(n,"--body").trim();(o.startsWith(sn.trim())||o.includes(Hi))&&l(`${a} adds the ${sn.trim()} prefix and the claude-bridge marker itself; pass only your words`);const c=s.repo||ie(D(e),{})||l(`${a} needs a board on GitHub; this board has no repo there`);try{const d=await Wi({repo:c,pr:s.number,replyTo:i,words:o,login:me(await Q(e)).github_login});return{lines:[`posted on PR ${s.repo?t:`#${t}`}: ${d}`]}}catch(d){l(d.message)}}r(Po,"postPR");const Xn=r(async e=>An((await Q(e)).tickets),"boardSource");async function Ao(e,t,{afterApproval:i,confirmed:n}={}){D(e);const{wave:a,item:s}=await P(e,t);Ze(s)||l(`${t} has no PR`),C.includes(s.status)&&l(`${t} is ${s.status}`);const o=ie(e,a),c=Ge(s,o);c.length||l(`${t} has no repo`);const d=new Date().toISOString();let u;try{u=await Promise.all(c.map(v=>nn({repo:v.repo,pr:v.number,since:d})))}catch(v){l(v.message)}const p=u.map(({view:v})=>v);if(on(p)){const v=c.filter((_,R)=>p[R].state==="MERGED");return Se(e,t,{status:"merged",text:`PR${v.length>1?"s":""} ${Ae(v,o)} ${v.length>1?"were":"was"} already merged.`})}const f=c.map((v,_)=>({...v,...u[_]})).filter(({view:v})=>v.state==="OPEN"),w=[];let m=!1;for(const v of f){const{view:_,reviews:R}=v,{failing:j,pending:O}=an(v,{pr:v.number,since:d,me:null}),E=yi({..._,checks:{state:j.length?"fail":O?"pending":"pass",failing:j,pending:O}}),F=R.filter(K=>K.state==="APPROVED").sort((K,U)=>String(K.submitted_at).localeCompare(String(U.submitted_at))).at(-1)?.commit_id,se=!!(F&&_.headRefOid&&F!==_.headRefOid);m||=se,se&&i===void 0&&E.push('commits after the approval: pass --after-approval "<why they are trivial>" when they are, else ask the approver to look again'),w.push(...c.length>1?E.map(K=>`${Ae([v],o)} ${K}`):E)}f.length||w.push(c.length>1?"every PR is closed":"PR is closed"),i!==void 0&&!m&&l("--after-approval: no commit came after the approval; drop it");const g=s.manual_steps?.length?n===void 0?null:await Wn(e,n,"--confirmed"):void 0;if(n!==void 0&&g===void 0&&l(`--confirmed: ${t} has no manual steps; drop it`),g===null&&w.push(`manual steps: merge only on the owner's word, with --confirmed "<their words>"`),w.length){const v=`Not merged: ${w.join("; ")}.`;await Ua(e,t)&&await Ne(e,t,{text:v}),l(v)}const $=[];for(const v of f){const _=await new Promise(R=>{Gt("gh",["pr","merge",String(v.number),"--repo",v.repo,"--squash"],{encoding:"utf8",timeout:12e4},(j,O,E)=>R(j?String(E||j.message).trim().split(`
|
|
11
|
+
`)[0]:null))});_&&l(`gh pr merge ${Ae([v],o)} failed: ${_}${$.length?` (merged ${Ae($,o)} before it)`:""}`),$.push(v)}const y=[zn($,o),i!==void 0&&`After the approval: ${S(i,"--after-approval")}`,g&&`The owner's word on the manual steps: "${g}"`];return Se(e,t,{status:"merged",text:y.filter(Boolean).join(" ")})}r(Ao,"mergeCard");async function Ua(e,t){const i=(await W(h.join(e,"data","events.jsonl"))).filter(a=>a.kind==="card-merge"&&a.data?.key===t).at(-1)?.ts,n=(await W(H(e,t))).filter(a=>(a.who||"claude")==="claude"&&a.kind!=="status").at(-1)?.ts;return!!i&&String(i)>String(n||"")}r(Ua,"ownerMergeWaits");async function Ft(e,t,i,n,a,s,o){try{const c=await Q(e),d=gt(c.tickets||{source:"internal"}),u=r((m,g)=>m.internal===!0?String(g.id).replace(/^S(\d+)$/,`${d.prefix}-$1`):null,"ownId");a===void 0&&d.source==="internal"&&$t(d,u(i,n))&&(a=u(i,n));let p=0;if(d.source==="internal")for(const m of await T(e)){const g=await k(m.file,{});for(const $ of g.items||[]){const y=[$.ticket,u(g,$)];if(p=Math.max(p,...y.map(v=>$t(d,v)||0)),a!==void 0&&`${m.slug}/${$.id}`!==t&&y.includes(a))throw Object.assign(new te(`${a} is already the ticket of ${m.slug}/${$.id}`),{held:!0})}}const f=a===void 0&&d.source!=="internal"&&n.plan?await A.readFile(await Ce(o,n.plan),"utf8"):n.notes||"",w=await Ei(d,{ticket:a,title:n.title,body:f,repo:ie(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 te)throw c;l(c.message)}}r(Ft,"assignTicket");const _e=r((e,t)=>kt(h.join(e||Xe(),"tickets"),t,{heartbeat:!0}),"ticketWrite"),Se=r((e,t,i,n)=>i.ticket!==void 0||i.fromAsk!==void 0?_e(e,()=>Jn(e,t,i,n)):Jn(e,t,i,n),"setItem"),Ro=r((e,t,i,n)=>_e(e,()=>Wa(e,t,i,n)),"addItem");async function Wa(e,t,{title:i,notes:n,deps:a,notesFile:s,ticket:o,pickUpAt:c,parent:d,size:u,model:p},f){const{wave:w,id:m}=t.includes("/")?B(t):{wave:t};m!==void 0&&re(m,"the item id");const{source:g,wave:$}=await ye(e,w);m!==void 0&&$.items.some(v=>v.id===m)&&l(`wave "${w}" already has an item "${m}"`);const y={id:m,title:S(i,"--title"),created:new Date().toISOString()};return d!==void 0&&(y.parent=await Vn(e,w,null,d)||l("--parent takes a card, such as <wave>/<id>")),n!==void 0&&(y.notes=S(n,"--notes")),Qn(y,{size:u,model:p}),a!==void 0&&(y.deps=await Zn(e,w,a)),s!==void 0&&(await Ce(g.dir,s),y.plan=h.normalize(s)),y.status=y.deps?.length?"waiting":"planned",c!==void 0&&st(y,{until:Gn(c)}),await Ft(e,t,$,y,o,f,g.dir),m===void 0&&(y.id=Ci(y.ticket),$.items.some(v=>v.id===y.id)&&l(`wave "${w}" already has an item "${y.id}"; link ${y.ticket} with item add ${w}/<id> --ticket ${y.ticket}`)),$.items.push(y),await x(g.file,$),{file:g.file,key:`${w}/${y.id}`,lines:[]}}r(Wa,"createItem");const No=r((e,{dryRun:t=!1}={},i)=>_e(D(e),()=>Ha(e,t,i)),"backfillTickets");async function Ha(e,t,i){const n=await Xn(e)||l("this board has no ticket source yet; pick one in Board settings first"),a=new Set,s=[];for(const p of await T(e)){const f=await k(p.file,null);if(Array.isArray(f?.items)){s.push({entry:p,wave:f});for(const w of f.items)w.ticket&&a.add(w.ticket),f.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,f)=>f.ticket&&!(n.source!=="internal"&&p.internal===!0&&f.ticket===f.id),"ticketed");for(const{entry:p,wave:f}of s)for(const w of f.items){if(d(f,w)||C.includes(w.status))continue;const m=`${p.slug}/${w.id}`,g=[...Bi(n,w.title),...(w.linear||[]).filter($=>$t(n,$))].find($=>n.source!=="internal"||!a.has($));if(t){c[g?"linked":"created"]++,o.push(`${m} would ${g?`link ${g}`:"get a new ticket"}`);continue}try{await Ft(e,m,f,w,g,i,p.dir),await x(p.file,f),a.add(w.ticket),c[g?"linked":"created"]++,o.push(`${m} ${g?"linked":"created"} ${w.ticket}`)}catch($){if(!($ instanceof te))throw $;c.failed++,o.push(`${m} failed: ${$.message}`)}}const u=t?`would link ${c.linked}, create ${c.created}`:`linked ${c.linked}, created ${c.created}, failed ${c.failed}`;return{lines:[...o,`backfill${t?" (dry run)":""}: ${u}`]}}r(Ha,"backfill");const Eo=r(e=>_e(e,async()=>{const t=[];for(const i of await T(e)){const n=await k(i.file,null);if(!Array.isArray(n?.items)||!Array.isArray(n.followups))continue;const a=n.followups.find(d=>n.items.some(u=>u.id===d.id));a&&l(`${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:u,from:p,filed:f,done:w,...m}of n.followups){const g=w?e&&(await W(H(e,`${i.slug}/${d}`))).map($=>$.ts).filter(Boolean).sort().pop()||f:null;s.push({id:d,title:u,...f?{created:f}:{},...p?{parent:V(i.slug,p)}:{},status:w?"done":"planned",...g?{status_at:g}:{},...m})}const{followups:o,...c}=n;await x(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"),ot=r(e=>h.join(e,"data","done-archive.json"),"archiveFile"),Mt=r((e,t)=>kt(h.join(e,"done-archive"),t,{heartbeat:!0}),"archiveWrite"),rt=r(async e=>(await k(ot(e),{cards:[]})).cards||[],"readArchive"),ct=r(e=>`${e.closed_at||""}|${e.key}`,"archiveRank"),Ka=r((e,t)=>ct(e)<ct(t)?1:ct(e)>ct(t)?-1:0,"byClosed"),Ut=r(e=>h.join(e,"data","done-archive-count.json"),"archiveCountFile");async function Lo(e){const t=(await k(Ut(e),null))?.count;if(Number.isInteger(t))return t;const i=(await rt(e)).length;return await x(Ut(e),{count:i}),i}r(Lo,"archiveCount");async function Wt(e,t){t.length?await x(ot(e),{cards:t.sort(Ka)}):await A.rm(ot(e),{force:!0}),await x(Ut(e),{count:t.length})}r(Wt,"writeArchive");const Io=r((e,t)=>Mt(e,async()=>{const i=new Map(t.map(f=>[f.key,f])),n=new Date().toISOString(),a=[],s=[];for(const f of await T(e)){const w=await k(f.file,null);if(!Array.isArray(w?.items))continue;const m=w.items.filter(g=>i.has(`${f.slug}/${g.id}`)&&C.includes(g.status));if(m.length){for(const g of m)a.push({...i.get(`${f.slug}/${g.id}`),wave:f.slug,archived_at:n,item:g});s.push([f.file,{...w,items:w.items.filter(g=>!m.includes(g))}])}}if(!a.length)return[];const o=await k(L(e),{items:[]}),c=new Map(a.map(f=>[`item:${f.key}`,f]));for(const f of a)f.questions=(o.items||[]).filter(w=>w.topic===`item:${f.key}`&&w.resolved);const d=await pe(e);for(const f of a)f.asks=f.item.from_ask&&M(d,f.item.from_ask)===void 0?[f.item.from_ask]:[];const u=new Set(a.map(f=>f.key));await Wt(e,[...(await rt(e)).filter(f=>!u.has(f.key)),...a.map(({key:f,wave:w,archived_at:m,closed_at:g,item:$,questions:y,asks:v})=>({key:f,wave:w,archived_at:m,closed_at:g,item:$,questions:y,asks:v}))]),a.some(f=>f.asks.length)&&await x(ce(e),{links:{...d,...Object.fromEntries(a.flatMap(f=>f.asks.map(w=>[w,f.key])))}});for(const[f,w]of s)await x(f,w);const p=(o.items||[]).filter(f=>!(c.has(f.topic)&&f.resolved));return p.length!==(o.items||[]).length&&await x(L(e),{...o,items:p}),[...u]}),"archiveCards");async function za(e,t){return!e||!He(ot(e))?!1:Mt(e,async()=>{const i=await rt(e),n=i.find(f=>f.key===t);if(!n)return!1;const a=(await T(e)).find(f=>f.slug===n.wave),s=a&&await k(a.file,null);if(!Array.isArray(s?.items))return!1;s.items.some(f=>f.id===n.item.id)||await x(a.file,{...s,items:[...s.items,n.item]});const o=await k(L(e),{items:[]}),c=new Set((o.items||[]).map(f=>f.id)),d=(n.questions||[]).filter(f=>!c.has(f.id));d.length&&await x(L(e),{...o,items:[...o.items||[],...d]});const u=await pe(e),p=(n.asks||[]).filter(f=>M(u,f)===t);return p.length&&await x(ce(e),{links:Object.fromEntries(Object.entries(u).filter(([f])=>!p.includes(f)))}),await Wt(e,i.filter(f=>f!==n)),!0})}r(za,"restoreArchived");const Yn=["push_tag","no_change"],ei=r(e=>Object.fromEntries(Object.entries(e).filter(([t])=>!Yn.includes(t))),"stripQuestion"),ti=r(({summary:e,...t})=>t,"stripItem"),ni=r(e=>Yn.some(t=>t in e),"holdsDead"),Bo=r(e=>Mt(e,async()=>{for(const n of await T(e)){const a=await k(n.file,null);Array.isArray(a?.items)&&a.items.some(s=>"summary"in s)&&await x(n.file,{...a,items:a.items.map(ti)})}const t=await k(L(e),null);(t?.items||[]).some(ni)&&await x(L(e),{...t,items:t.items.map(ei)});const i=await rt(e);i.some(n=>"card"in n||"summary"in n.item||(n.questions||[]).some(ni))&&await Wt(e,i.map(({card:n,...a})=>({...a,...n?{closed_at:a.closed_at||n.sortAt}:{},item:ti(a.item),questions:(a.questions||[]).map(ei)})))}),"stripDeadFields"),ii=r(e=>`lineup-${e}.md`,"lineupNoteFile"),Ja=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:l(`a wave's slug uses only a-z, 0-9 and "-", not "${e}"`),"lineupSlug");async function lt(e,t){for(const i of await T(e)){const n=await k(i.file,null),a=(n?.lineups||[]).find(s=>s.slug===t);if(a)return{source:i,wave:n,lineup:a}}return l(`this board has no wave "${t}"; lineup create makes one`)}r(lt,"findLineup");async function Qa(e,t){(await Q(e)).items!=="waves"&&l(`topic wave:${t} is for a board over ~/.claude/waves; a lineup's triage uses lineup:<slug>`),await ye(e,t)}r(Qa,"triagedWave");function Te(e,t,i,n){return i.map(a=>{const{wave:s,id:o}=B(a);s!==e.slug&&l(`${a} is not in wave file "${e.slug}"; a wave's cards share one file`),t.items.some(d=>d.id===o)||l(`wave "${s}" has no item "${o}"`);const c=(t.lineups||[]).find(d=>d.slug!==n&&!d.finished_at&&wt(d).flat().includes(o));return c&&l(`${a} is already in the wave "${c.slug}"; lineup remove ${c.slug} ${a} first`),o})}r(Te,"lineupCards");function ai(e){const t=[e??[]].flat().map(i=>String(i).split(",").map(n=>n.trim()).filter(Boolean));return(!t.length||t.some(i=>!i.length))&&l("--phase takes one phase's cards, as <wave>/<id>,<wave>/<id>, once per phase in build order"),t}r(ai,"phaseKeys");function si(e,t,i,n){const a=new Set(i.map(s=>`${e.slug}/${s}`));for(const s of i){const o=t.items.find(u=>u.id===s),c=(o.deps||[]).filter(u=>!a.has(V(e.slug,u))),d=n.findIndex(u=>u.includes(s));d>0&&c.push(...n[d-1]),c.length?o.deps=c:delete o.deps}}r(si,"chain");async function oi(e,t,i,n,a){a!==void 0&&await vt(h.join(t.dir,ii(n.slug)),`${S(a,"the note").replace(/\n*$/,`
|
|
12
|
+
`)}`),await x(t.file,i);const{tickets:s}=me(await Q(e));return{file:t.file,lines:[`order: ${ri(i,n.phases,s)}`]}}r(oi,"writeLineup");const ri=r((e,t,i)=>t.map(n=>n.map(a=>Qe(e,e.items.find(s=>s.id===a)||{id:a},i)).join(", ")).join(" \u2192 "),"orderLine");async function Co(e,t,{title:i,goal:n,phases:a,note:s}){Ja(t);const o=ai(a);s===void 0&&l("lineup create needs the wave note, as --note <text> or --file <path>");const c=await T(e);c.some(w=>w.slug===t)&&l(`"${t}" is a wave file on this board; pick another slug`);for(const w of c)((await k(w.file,null))?.lineups||[]).some(m=>m.slug===t)&&l(`this board already has a wave "${t}"`);const d=c.find(w=>w.slug===B(o[0][0]).wave)||l(`this board has no wave "${B(o[0][0]).wave}"`),{wave:u}=await ye(e,d.slug),p=o.map(w=>Te(d,u,w,t));new Set(p.flat()).size!==p.flat().length&&l("--phase names a card twice");const f={slug:t,title:S(i,"--title"),goal:S(n,"--goal"),phases:p,created:new Date().toISOString()};return si(d,u,p.flat(),p),u.lineups=[...u.lineups||[],f],oi(e,d,u,f,s)}r(Co,"createLineup");async function To(e,t,{add:i,after:n,remove:a,phases:s,title:o,goal:c,note:d}){[i,a,s,o,c,d].every(m=>m===void 0)&&l("nothing to change: pass --add, --remove, --phase, --title, --goal, --note or --file"),n!==void 0&&i===void 0&&l("--after goes with --add");const{source:u,wave:p,lineup:f}=await lt(e,t);f.finished_at&&l(`the wave "${t}" is finished`),f.phases=wt(f),delete f.items;const w=f.phases.flat();if(i!==void 0){const[m]=Te(u,p,[i],t);w.includes(m)&&l(`${i} is already in the wave "${t}"`);const g=n===void 0?f.phases.length:f.phases.findIndex($=>$.includes(Te(u,p,[n],t)[0]))+1;g===0&&l(`--after ${n} is not in the wave "${t}"`),f.phases.splice(g,0,[m])}if(a!==void 0){const[m]=Te(u,p,[a],t);w.includes(m)||l(`${a} is not in the wave "${t}"`),w.length===1&&l(`${a} is the wave's last card; lineup finish ${t} ends the wave`),f.phases=f.phases.map(g=>g.filter($=>$!==m)).filter(g=>g.length)}if(s!==void 0){const m=ai(s).map($=>Te(u,p,$,t)),g=m.flat();(g.length!==w.length||new Set(g).size!==g.length||g.some($=>!w.includes($)))&&l("--phase lists each of the wave's cards once"),f.phases=m}return o!==void 0&&(f.title=S(o,"--title")),c!==void 0&&(f.goal=S(c,"--goal")),si(u,p,[...new Set([...w,...f.phases.flat()])],f.phases),oi(e,u,p,f,d)}r(To,"changeLineup");async function qo(e,t){const{source:i,wave:n,lineup:a}=await lt(e,t);return a.finished_at=new Date().toISOString(),await x(i.file,n),{file:i.file}}r(qo,"finishLineup");async function Fo(e,t,i){const{source:n,wave:a,lineup:s}=await lt(e,t),o=await A.readFile(h.join(n.dir,ii(t)),"utf8").catch(()=>"");return{title:s.title,markdown:`**Goal** ${s.goal}
|
|
13
|
+
|
|
14
|
+
**Order** ${ri(a,wt(s),i)}
|
|
15
|
+
|
|
16
|
+
${o}`}}r(Fo,"readLineupNotes");async function G(e,t,i,n="main"){const{id:a}=B(t);/^[\w-]{1,40}$/.test(n)||l(`--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 P(e,t);return h.join(o.dir,"agents",s)}r(G,"builderFile");const ci=r((e,t)=>typeof e=="string"&&/^\d{4}-\d\d-\d\dT/.test(e)&&!Number.isNaN(Date.parse(e))?new Date(e).toISOString():l(`${t} takes an ISO time such as 2026-10-23T16:30:00-07:00`),"isoTime"),Ga=["reading","building","testing","checking","landing"],li=80,di="reading, building, testing, checking or landing";function ui(e,t,i,n){e||l(`${n} needs --site, since the line goes to the item's thread`),t!==void 0&&!Ga.includes(t)&&l(`--phase takes ${di}, not "${t}"`);const a=S(i,n);return a.length>li&&l(`${n} is ${a.length} characters; the cap is ${li}. Say it shorter.`),{phase:t,detail:a}}r(ui,"progressLine");const fi=r((e,t,i)=>t?{detail:e.detail}:(e.phase===void 0&&l(`${i} needs --phase ${di}`),e),"forAgent");async function Ht(e,t,i="main",n,{phase:a,detail:s},o){const c=Je(n),d=c||i==="main"?"":ze(n)||n.kind||i,u=c?xi[n.kind]:[d,a].filter(Boolean).join(" "),p=u?`${u[0].toUpperCase()}${u.slice(1)}: ${s}`:s,f=H(e,t);(await W(f)).filter(w=>w.kind==="status").pop()?.text!==p&&await ee(f,{ts:o,who:"claude",kind:"status",...a?{phase:a}:{},...c?{planner:!0}:{},text:p})}r(Ht,"statusToThread");const Za={prototype:"sonnet",grill:"opus"};async function Va(e,t,{label:i,phase:n,progress:a,dir:s,agent:o="main",kind:c,model:d,eta:u,worktree:p,ownWorktree:f,dispatch:w}){const m=Yt();if(m.state==="paused")throw new te(en(m));n!==void 0&&a===void 0&&l('--phase goes with --progress "<detail>"');const g=Z(a,J=>ui(e,n,J,"--progress")),$=await G(e,t,s,o),y=new Date().toISOString(),v=Z(c,J=>/^[\w.-]{1,30}$/.test(J)?J:l("--kind takes a short word such as claude or codex")),_=pt(v),R=d??(v==="opus"||v==="sonnet"?v:void 0),j=["build",...is].includes(_),O=await k($,null),E=r(J=>["tap","hub"].includes(J?.dispatched)&&!J.done,"tapOpen");if(w==="session"&&E(O))return{file:$,alreadyOpen:!0,lines:[]};o!=="main"&&E(await k(await G(e,t,s),null))&&l(`the Implement tap opened ${t}'s main builder record and no agent has taken it up; run builder start ${t} without --agent first`);const F=O?.dispatched&&!O.done?O:null,se=j&&!s?(await P(e,t)).item:void 0,K=O&&he(O).length?O:null,U=F&&_==="build"?{kind:pt(F.kind),model:F.model,reason:{tap:"The owner's Build tap picked it.",hub:"The board's pull picked it."}[F.dispatched]??"The session's dispatch picked it."}:j?tn({card:se??{}}):null;U&&_!=="build"&&_!==U.kind&&l(`--kind ${_}: ${U.kind} builds now. ${U.reason} Pass --kind ${U.kind}, or --kind build.`);const q=U?U.kind:_,Fe=g&&fi(g,Je({kind:q}),"--progress");d!==void 0&&!["opus","sonnet"].includes(d)&&l("--model takes opus or sonnet"),R!==void 0&&q==="codex"&&l("--model is for kind claude; codex needs none");const X=q==="claude"?Pi():"auto";R!==void 0&&X!=="auto"&&R!==X&&l(`--model ${R}: the hub's Claude model is ${X==="opus"?"Opus":"Sonnet"}. Pass --model ${X}, or no --model.`);const je=q==="claude"?R??U?.model??(F&&pt(F.kind)==="claude"?F.model:void 0)??"opus":Je({kind:q})?R??(F?.kind===q?F.model:void 0)??Za[q]:void 0,oe={label:i===void 0?"Builder":S(i,"--label"),...q?{kind:q}:{},...je?{model:je}:{},started:y,...u===void 0?{}:{eta:ci(u,"--eta")},...w?{dispatched:w}:{},...K?{calls:K.calls,...K.verified?{verified:K.verified}:{}}:{}},ge=j&&!s&&![...C,"building","fixing"].includes(se.status),$e=ge?await Se(e,t,{status:"building"}):{lines:[]};let I,z=null;const de=f&&j&&o==="main"&&O?.worktree?.path&&He(O.worktree.path)?O.worktree:null,b=(F??K)?.worktree??de,Me=p??(j&&o==="main"&&!b?f:void 0);try{if(Me){const J=await ae(h.dirname(h.dirname($)),B(t).id),dt=(await Promise.all(J.filter(Y=>Y!==$).map(Y=>k(Y,null)))).filter(Y=>Y?.worktree?.path&&!Y.done).map(Y=>Y.worktree.path);({remade:z,...I}=await Me({busy:dt}))}else I=b}catch(J){throw ge&&await ns(e,t,se),J}await x($,I?{...oe,worktree:I}:oe),z&&e&&await Ne(e,t,{text:z}),Fe&&await Ht(e,t,o,oe,Fe,y);const Ue=await Tt(e,t,"Acted on: builder started");return e&&j&&await Pa(e,t),{file:$,kind:q??null,model:je??null,reason:U?.reason??null,worktree:I?.path??null,lines:[..._==="build"?[`kind: ${q}. ${U.reason}`]:[],...ze(oe)?[`model: ${ze(oe)}`]:[],...e?[`ref: ${await ne(e,t)}`]:[],...I?[`worktree: ${I.path}`]:[],...z?[z]:[],...$e.lines,...Ue]}}r(Va,"startBuilder");async function Xa(e,t,i,n="tap"){try{const{held:a,blockers:s}=await Ya(e,t);if(a&&await Fa(e,"card-hold",{key:t,hold:!1}),s.length)return await es(e,t,s);try{const o=await Va(e,t,{kind:"build",worktree:i,dispatch:n});return{key:t,ref:await ne(e,t),kind:o.kind,...o.model?{model:o.model}:{},reason:o.reason,worktree:o.worktree}}catch(o){throw a&&await ts(e,t,a),o}}catch(a){if(!(a instanceof te))throw a;return await ee(H(e,t),{ts:new Date().toISOString(),who:"claude",kind:Zt,text:`Couldn't start implementing: ${a.message}`}),null}}r(Xa,"startTappedBuild");async function Ya(e,t){const i=Yt();i.state==="paused"&&l(en(i));const{item:n}=await P(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&&n.status&&n.status!=="held"&&l("it's held. Unhold it, then tap Implement."),await Un(e,t)&&l("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 at(e,B(t).wave,n)).map(o=>ne(e,o)));return{held:a,blockers:s}}r(Ya,"tapRefusals");async function es(e,t,i){const{source:n,wave:a,item:s}=await P(e,t);return s.queued_at||(s.queued_at=new Date().toISOString(),await x(n.file,a),await ee(H(e,t),{ts:s.queued_at,who:"claude",text:`Implement queued: it starts once ${i.join(", ")} ${i.length===1?"is":"are"} done.`})),{key:t,ref:await ne(e,t),queued:i}}r(es,"queueBuild");async function Mo(e,t,i){let n;try{n=await Xa(e,t,i)}catch(a){await ee(H(e,t),{ts:new Date().toISOString(),who:"claude",kind:Zt,text:`Couldn't start implementing: ${a.message}`}),n=null}if(!n){const{source:a,wave:s,item:o}=await P(e,t);delete o.queued_at,await x(a.file,s)}return n}r(Mo,"startQueuedBuild");async function ts(e,t,i){const{source:n,wave:a,item:s}=await P(e,t);for(const o of Object.keys(i))i[o]===void 0?delete s[o]:s[o]=i[o];await x(n.file,a)}r(ts,"restoreHold");async function ns(e,t,i){const{source:n,wave:a,item:s}=await P(e,t);for(const o of["status","status_at"])i[o]===void 0?delete s[o]:s[o]=i[o];await x(n.file,a)}r(ns,"restoreStatus");const is=["codex","claude"];async function Uo(e,t){const{item:i}=await P(e,t),{kind:n,model:a,reason:s}=tn({card:i});return{lines:[`${[n,a].filter(Boolean).join(" ")}: ${s}`]}}r(Uo,"pickBuilder");async function wi(e,t,{dir:i}={}){const{id:n}=B(t),a=h.dirname(await G(e,t,i)),s=await ae(h.dirname(a),n);return(await Promise.all(s.map(c=>k(c,null)))).filter(c=>c?.worktree?.path).sort((c,d)=>String(d.started).localeCompare(String(c.started)))[0]?.worktree??null}r(wi,"recordedWorktree");async function Wo(e,t,i,{phase:n,dir:a,agent:s="main",eta:o,clearEta:c}){o!==void 0&&c&&l("pass --eta or --clear-eta, not both");const d=ui(e,n,i,"builder progress"),u=await G(e,t,a,s),p=await k(u,null);(!p?.started||p.done)&&l(`builder progress needs an open builder record on ${t}; run builder start first`);const f=fi(d,Je(p),"builder progress");return o!==void 0&&(p.eta=ci(o,"--eta")),c&&delete p.eta,(o!==void 0||c)&&await x(u,p),await Ht(e,t,s,p,f,new Date().toISOString()),u}r(Wo,"builderProgress");const he=r(e=>(e?.calls||[]).filter(t=>!e.verified||t.at>e.verified.at),"unverifiedCalls");async function Ho(e,t,i,{dir:n,agent:a="main"}){const s=S(i,"builder call");/\n/.test(s)&&l("builder call takes one line; record each call on its own");const o=await G(e,t,n,a),c=await k(o,null);return(!c?.started||c.done)&&l(`builder call needs an open builder record on ${t}; run builder start first`),await x(o,{...c,calls:[...c.calls||[],{at:new Date().toISOString(),text:s}]}),o}r(Ho,"recordCall");async function qe(e,t){const i=await W(H(e,t)),n=new Map(i.filter(a=>a.kind===Ke.answer).map(a=>[a.ask,a]));return i.filter(a=>a.kind===Ke.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(qe,"builderAsks");const as=r(e=>ke().find(t=>Pe(t.dir,e))??{dir:e},"boardTarget");async function Ko(e,t,i,{agent:n="main"}){D(e);const a=S(i,"builder ask");/\n/.test(a)&&l("builder ask takes one line; ask each question on its own");const s=await k(await G(e,t,void 0,n),null);(!s?.started||s.done)&&l(`builder ask needs an open builder record on ${t}; run builder start first`);const o=`ask${(await qe(e,t)).length+1}`;return await ee(H(e,t),{ts:new Date().toISOString(),who:`${ze(s)||s.kind||"Claude"} builder`,kind:Ke.ask,ask:o,agent:n,text:a}),await De(as(e),"builder-ask",{key:t,ask:o,agent:n,text:a}),{id:o,lines:[]}}r(Ko,"askOrchestrator");async function zo(e,t,i,n,{deferred:a=!1}={}){D(e);const s=S(n,"--text"),o=(await qe(e,t)).find(d=>d.id===i)||l(`no ask "${i}" on ${t}; builder asks ${t} lists them`);o.answer&&l(`${i} on ${t} is already ${o.deferred?"deferred":"answered"}`);const c=new Date().toISOString();if(await ee(H(e,t),{ts:c,who:"claude",kind:Ke.answer,ask:i,...a?{deferred:!0}:{},text:s}),a){const d=await G(e,t,void 0,o.agent),u=await k(d,null);u&&await x(d,{...u,calls:[...u.calls||[],{at:c,text:`Question: ${o.text} (deferred to the owner)`}]})}return{lines:[]}}r(zo,"answerBuilder");async function Jo(e,t){D(e);const i=await qe(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(Jo,"listAsks");async function ss(e,t,i){const n=t.topic.startsWith("item:")&&!t.topic.startsWith("item:ask:")?t.topic.slice(5):l(`${i}.ask goes on a card's question, topic item:<wave>/<id>`),a=(await qe(e,n)).find(s=>s.id===t.ask)||l(`${i}.ask names no ask "${t.ask}" on ${n}; builder asks ${n} lists them`);a.deferred||l(`${i}.ask names ${a.id}, which the orchestrator ${a.answer?"answered":"has not deferred"}; only a deferred ask goes to the owner`)}r(ss,"checkAskQuestion");async function Qo(e,t,{dir:i,agent:n="main"}){const a=await k(await G(e,t,i,n),null),s=a?.calls||[];if(!s.length)return{lines:["no judgment calls recorded"]};const{verified:o}=a,c=he(a).length?"not verified yet":`verified ${o.at}${o.note?`: ${o.note}`:""}`;return{lines:[...s.map((d,u)=>`${u+1}. ${d.text}`),c]}}r(Qo,"listCalls");async function Go(e,t,{dir:i,agent:n="main",note:a}){const s=await G(e,t,i,n),o=await k(s,null);if(he(o).length||l(`${t} has no unverified judgment calls${n==="main"?"":` from agent ${n}`}; builder calls ${t} lists them`),e){const c=new Set((await k(L(e),{items:[]})).items?.filter(u=>u.topic===`item:${t}`&&u.ask).map(u=>u.ask)),d=(await qe(e,t)).find(u=>u.deferred&&u.agent===n&&!c.has(u.id));d&&l(`${d.id} on ${t} was deferred to the owner; add its question with "ask": "${d.id}" first`)}return await x(s,{...o,verified:{at:new Date().toISOString(),...a===void 0?{}:{note:S(a,"--note")}}}),s}r(Go,"verifyCalls");async function Zo(e,t){const{source:i,item:n}=await P(e,t),a=r((o,c)=>[(o.options||[]).find(d=>d.id===c.choice)?.label,c.note].filter(Boolean).join(": ")||"answered","said"),s=(await le(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 ne(e,t),title:n.title||t,notes:n.plan?h.join(i.dir,n.plan):null,worktree:(await wi(e,t))?.path??null,answers:s}}r(Zo,"briefFacts");async function Vo(e){const t=[];for(const i of await T(e)){const n=await k(i.file,null);for(const a of n?.items||[]){if(a.status!=="checking")continue;const s=(await Promise.all((await ae(i.dir,a.id)).map(u=>k(u,null)))).filter(Boolean),o=s.map(u=>u.verified?.at).filter(Boolean).sort().at(-1);if(!o||s.some(u=>he(u).length))continue;const c=`${i.slug}/${a.id}`;if((await le(e,c)).asked.some(u=>u.state==="open"))continue;const d=s.filter(u=>u.worktree?.path).sort((u,p)=>String(p.started).localeCompare(String(u.started)))[0];d&&He(d.worktree.path)&&t.push({key:c,verified:o,model:d.model})}}return t}r(Vo,"landable");async function Xo(e,t,{dir:i}={}){const{id:n}=B(t),a=await ae(h.dirname(h.dirname(await G(e,t,i))),n),s=[];for(const o of a){if(!he(await k(o,null)).length)continue;const c=h.basename(o,".json").slice(String(n).length+1)||"main";s.push(c==="main"?"":` --agent ${c}`)}s.length&&l(`land: ${t} has judgment calls nobody verified yet; check them with ${s.map(o=>`builder calls ${t}${o}`).join(", ")}, then run ${s.map(o=>`builder verified ${t}${o}`).join(", ")}`)}r(Xo,"checkCallsVerified");async function Yo(e,t,{dir:i,agent:n="main",next:a}){const s=await k(await G(e,t,i,n),null),o=s&&!s.done?he(s):[];if(!o.length)return null;a!==void 0&&l(`builder done sets ${t}'s Next itself while its record has judgment calls; run it without --next`);const c=o.some(u=>u.text.startsWith("Question:"))?"Answer the builder's questions":"Verify the builder's judgment calls",d=await Se(e,t,{status:"checking",next:c});return{lines:[`handed back: checking, Next "${c}"`,...d.lines]}}r(Yo,"handBackCalls");async function er(e,t,{agent:i="main"}){if(i!=="main")return!1;const n=await G(e,t,void 0,i),a=await k(n,null);if(!a?.started||a.done||!a.worktree?.path)return!1;const{id:s}=B(t);for(const o of await ae(h.dirname(h.dirname(n)),s)){const c=o===n?null:await k(o,null);if(!c?.started||c.done||c.kind==="install")continue;const d=h.basename(o,".json").slice(String(s).length+1);l(`builder done would finish ${t} while agent ${d} is still open; run builder done ${t} --agent ${d} first`)}return!0}r(er,"finishesCard");async function tr(e,t){const{source:i,item:n}=await P(e,t);if(n.status!=="checking"||(await Promise.all((await ae(i.dir,n.id)).map(o=>k(o,null)))).filter(Boolean).some(o=>he(o).length||o.started&&!o.done&&o.kind!=="install"))return null;const s=await wi(e,t);return!s||!He(s.path)?null:{waits:(await le(e,t)).asked.filter(o=>o.state==="open").map(o=>o.id)}}r(tr,"verifiedFinish");async function nr(e,t){const{item:i}=await P(e,t);if(i.status!=="done"||i.merged_at)return;const n=String(i.status_at||"");(await W(h.join(e,"data","events.jsonl"))).some(s=>s.kind==="item-message"&&s.data?.key===t&&String(s.ts)>n)&&l(`the owner reopened ${await ne(e,t)} with a message; it closes on the owner's Done`)}r(nr,"checkNotReopened");const os=["checking","done","merged","dropped"];async function ae(e,t){const i=h.join(e,"agents");return(await A.readdir(i).catch(()=>[])).filter(n=>n===`${t}.json`||n.startsWith(`${t}@`)&&n.endsWith(".json")).map(n=>h.join(i,n))}r(ae,"builderFiles");async function rs(e,t,i){const n=new Date().toISOString();for(const a of await ae(e,t)){const s=await k(a,null);if(!s||s.done||s.kind==="install"&&!i)continue;const{eta:o,...c}=s;await x(a,{...c,done:n})}}r(rs,"closeBuilders");async function cs(e,t,{dir:i,agent:n,startedBetween:a,note:s,next:o,requireNext:c=!1}){const d=await G(e,t,i,n),u=await k(d,null);if(!u||u.done)return d;if(a){const E=Date.parse(u.started);if(!(E>=a[0]&&E<=a[1]))return d}const{eta:p,...f}=u,w=new Date().toISOString(),m={...f,done:w,...s?{note:s}:{}},{id:g}=B(t),$=h.dirname(h.dirname(d)),y=await Promise.all((await ae($,g)).map(E=>E===d?m:k(E,null))),v=!y.some(E=>E?.started&&!E.done),_=i?{file:h.join($,"wave.json")}:(await P(e,t)).source,R=await k(_.file,null),j=R?.items?.find(E=>E.id===g);o!==void 0&&(qn(j||l(`${t} has no item to set --next on`),o,await Tn(e,t)),e&&await Hn(e,t,j,j.next));const O=v&&qt(j);return c&&j&&mt(j,y)&&l(`builder done closes ${t}'s last open builder, which leaves it In Progress with no Next; pass --next "<what happens next>", or hand it back with item set ${t} --status checking or land`),await x(d,m),s&&await Ht(e,t,n,u,{detail:s},w),(O||o!==void 0)&&await x(_.file,R),d}r(cs,"stopBuilder");async function ls(e){const t=[];for(const i of await T(e)){const n=await k(i.file,null);for(const a of n?.items||[])for(const s of await ae(i.dir,a.id)){const o=await k(s,null);if(!o?.started||o.done)continue;const c=h.basename(s,".json");t.push({key:`${i.slug}/${a.id}`,agent:c.slice(String(a.id).length+1)||"main",label:o.label||"Builder",started:o.started})}}return t}r(ls,"openBoardBuilders");async function ir(e){const t=await ls(e);for(const i of t)await cs(e,i.key,{agent:i.agent,note:"Board closed. Builder record stopped; work is kept for reopening."});return t}r(ir,"closeBoardBuilders");export{te as BoardError,pn as PUSH_LINE_MAX,jt as UPLOAD_LIMIT,qs as WORKING_KINDS,Xs as ackHello,Ro as addItem,ho as addPrototype,ko as addQuestions,so as addReviewers,uo as addRuleTo,mo as addTopic,zo as answerBuilder,Ne as appendThread,Io as archiveCards,Lo as archiveCount,ot as archiveFile,ct as archiveRank,Ko as askOrchestrator,_o as askedGate,No as backfillTickets,Q as boardConfig,ie as boardRepoOf,Zo as briefFacts,Wo as builderProgress,ne as cardName,To as changeLineup,Xo as checkCallsVerified,vo as checkClose,nr as checkNotReopened,ir as closeBoardBuilders,Co as createLineup,fo as editRuleIn,Ks as eventCard,ro as fallBackFromLinear,Ys as finishHandshake,qo as finishLineup,er as finishesCard,Yo as handBackCalls,hn as hiddenFile,T as itemSources,Vo as landable,ii as lineupNoteFile,Jo as listAsks,Qo as listCalls,go as listPrototypes,lo as listRules,Qs as markAsk,Fs as markWorking,Ao as mergeCard,zn as mergedLine,Eo as migrateFollowups,Gs as moveAsk,ls as openBoardBuilders,B as parseKey,Uo as pickBuilder,Oo as pickUp,Po as postPR,Do as prReport,la as pushLine,fa as pushUrl,rt as readArchive,pe as readAskLinks,et as readAskProgress,Et as readBoardRules,Nn as readBoardSettings,vn as readHandshake,pa as readHidden,k as readJSON,Fo as readLineupNotes,So as readNotes,nt as readPrototypes,Hs as readUpload,na as readWorking,Ho as recordCall,Fa as recordOwnerTap,aa as recordPush,Vs as recordReview,wi as recordedWorktree,zs as relateAsk,$o as removePrototype,oo as removeReviewers,wo as removeRuleFrom,ia as repliesSincePush,bo as resolveNoteAnswers,yo as resolveQuestion,xo as resolveRequestedQuestion,ya as reviewDigest,ao as reviewDigestSent,io as reviewDue,no as reviewReact,to as reviewSent,po as saveBoardRules,Le as saveBoardSettings,Ws as saveUpload,Ms as sendPush,co as setBoardSetting,Us as setHidden,Se as setItem,jo as setMainInstall,eo as setReviewers,xa as settingsLines,me as settingsOf,Cs as siteDir,ea as sitesDir,Va as startBuilder,Mo as startQueuedBuild,Xa as startTappedBuild,cs as stopBuilder,Bo as stripDeadFields,Bt as takeFile,H as threadFile,Bn as topicQuestions,Js as unfoldAsk,tr as verifiedFinish,Go as verifyCalls,Xe as wavesDir,ca as withReplies,Zs as writeChecked,Ts as writeReply};
|
package/board/tickets.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)[0]);let
|
|
3
|
-
`)[0]})`,"why");try{if(e.source==="linear"){const
|
|
1
|
+
var L=Object.defineProperty;var o=(e,t)=>L(e,"name",{value:t,configurable:!0});import{runRepo as $,validateGithub as O}from"../hub/repos.mjs";import{savedClient as k}from"../hub/linear.mjs";const i=o(e=>{throw Object.assign(new Error(e),{status:400})},"refuse");function N(e){(!e||typeof e!="object"||Array.isArray(e))&&i("Tickets must name a source.");const t={internal:["source","prefix"],github:["source","account"],linear:["source","workspace","team","project"]};if(Object.hasOwn(t,e.source)||i("Ticket source must be internal, github or linear."),Object.keys(e).some(n=>!t[e.source].includes(n))&&i("Unknown ticket source field."),e.source==="internal"){const n=e.prefix===void 0||e.prefix===""?"INT":e.prefix;return(typeof n!="string"||!/^[A-Z]{2,6}$/.test(n))&&i("Prefix must be 2\u20136 uppercase letters."),{source:"internal",prefix:n}}return e.source==="github"?e.account===void 0||e.account===""?{source:"github"}:((typeof e.account!="string"||!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e.account))&&i("The gh account must be a GitHub login."),{source:"github",account:e.account}):((typeof e.workspace!="string"||!/^[a-z\d][a-z\d-]*$/i.test(e.workspace))&&i("Linear needs a workspace."),(typeof e.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(e.team))&&i("Linear needs an uppercase team key."),(typeof e.project!="string"||!e.project.trim())&&i("Linear needs a project."),{source:"linear",workspace:e.workspace,team:e.team,project:e.project.trim()})}o(N,"ticketSource");const w=o(e=>{const t=e?Number(e[1]):null;return Number.isSafeInteger(t)?t:null},"numberOf"),y=o(e=>w(/^#?([1-9]\d*)$/.exec(String(e??""))),"issueNumber"),x=o(e=>e.source==="internal"?e.prefix||"INT":e.source==="linear"?e.team:null,"keyOf");function g(e,t){if(!e)return null;const n=x(e),r=String(t??"");return n?r.startsWith(`${n}-`)?w(/^([1-9]\d*)$/.exec(r.slice(n.length+1))):null:w(/^#([1-9]\d*)$/.exec(r))}o(g,"ticketIn");function C(e,t,{repo:n,key:r}={}){const a=g(e,t);if(!a)return null;if(e.source==="internal")return{id:`${x(e)}-${a}`,url:`#item=${encodeURIComponent(r)}`,source:"internal"};if(e.source==="github")return n?{id:`#${a}`,url:`https://github.com/${n}/issues/${a}`,source:"github"}:null;const l=`${e.team}-${a}`;return{id:l,url:`https://linear.app/${e.workspace}/issue/${l}`,source:"linear"}}o(C,"ticketBadge");async function j(e,t){if(!e.account)return t;const n=await t("gh",["auth","token","-u",e.account]);(n.status!==0||!n.stdout?.trim())&&i(`gh has no ${e.account} account; run gh auth login`);const r={...process.env,GH_TOKEN:n.stdout.trim()};return(a,l)=>t(a,l,{env:r})}o(j,"issueRunner");const T=o(async e=>{try{return await e()}catch(t){i(t.message)}},"linearCall");async function E(e,{ticket:t,title:n,body:r="",repo:a,next:l},{run:u=$,linearFetch:b}={}){if(e.source==="internal")return t!==void 0&&!g(e,t)&&i(`Ticket must match ${e.prefix}-<number>.`),{ticket:t??`${e.prefix}-${l}`,created:t===void 0};if(e.source==="linear"){t!==void 0&&!g(e,t)&&i(`Ticket must match ${e.team}-<number>.`);const c=k({fetch:b});if(c||i("Hub has no Linear key, so this board can\u2019t create Linear tickets; add it in Hub settings \u203A Linear, or pick another ticket source."),t===void 0)return{ticket:(await T(()=>c.createIssue({team:e.team,project:e.project,title:n,description:r}))).identifier,created:!0};const h=await T(()=>c.issue(t));return h||i(`Linear has no issue ${t}.`),h.team!==e.team&&i(`${t} is in Linear's ${h.team} team, not ${e.team}.`),{ticket:h.identifier,created:!1}}O(a);const s=t===void 0?null:y(t);t!==void 0&&!s&&i("GitHub ticket must be an issue number such as #41.");const p=s?["issue","view",String(s),"--repo",a,"--json","number,url"]:["issue","create","--repo",a,"--title",n,"--body",r],m=await(await j(e,u))("gh",p);m.status!==0&&i((m.stderr||"GitHub refused the issue.").trim().split(`
|
|
2
|
+
`)[0]);let f;if(s){let c;try{c=JSON.parse(m.stdout)}catch{i("GitHub returned an invalid issue.")}c.number!==s&&i("GitHub returned a different issue."),f=c.url}else f=m.stdout?.trim();const d=/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/([1-9]\d*)$/.exec(f||"");return(!d||d[1].toLowerCase()!==a.toLowerCase()||s&&Number(d[2])!==s||!y(d[2]))&&i("Ticket must be an issue in the board\u2019s repo."),{ticket:`#${d[2]}`,created:!s}}o(E,"resolveTicket");const Z=o(e=>e.startsWith("#")?`GH-${e.slice(1)}`:e,"cardIdOf");function R(e,t){const n=e.source==="github"?String(t??"").match(/(?<![\w#])#[1-9]\d*\b/g):String(t??"").match(/\b[A-Z][A-Z0-9]*-[1-9]\d*\b/g);return[...new Set((n||[]).filter(r=>g(e,r)))]}o(R,"titleTickets");function z({status:e,pr:t}){return["done","merged"].includes(e)?"done":["building","fixing"].includes(e)?"started":t!==void 0&&e===void 0?"review":null}o(z,"ticketTarget");const S={started:"In Progress",review:"In Review",done:"Done"};async function _(e,t,n,{repo:r,run:a=$,linearFetch:l}={}){const u=t.ticket;if(!e||!n||!u||!g(e,u)||e.source==="internal"||e.source==="github"&&n!=="done")return null;if(e.source==="github"&&!r)return`ticket ${u}: not closed, the board has no GitHub repo`;const b=o(s=>`ticket ${u}: could not move to ${S[n]} (${String(s.message||s).split(`
|
|
3
|
+
`)[0]})`,"why");try{if(e.source==="linear"){const f=k({fetch:l});if(!f)return`ticket ${u}: not moved, Hub has no Linear key`;if(!t.ticket_created){const c=await f.issue(u);if(!c)return`ticket ${u}: not moved, Linear has no such issue`;if(c.assignee!==f.saved.viewer?.id)return`ticket ${u}: left as it is, not assigned to you`}const d=await f.moveIssue(u,n);return d?`ticket ${u}: moved to ${d}`:null}const s=u.slice(1),p=await j(e,a);if(!t.ticket_created){const f=await p("gh",["issue","view",s,"--repo",r,"--json","state,assignees"]);if(f.status!==0)throw new Error(f.stderr||"gh issue view failed");const d=await p("gh",["api","user","--jq",".login"]);if(d.status!==0)throw new Error(d.stderr||"gh api user failed");const c=JSON.parse(f.stdout);if(c.state!=="OPEN")return null;if(!(c.assignees||[]).some(h=>h.login?.toLowerCase()===d.stdout.trim().toLowerCase()))return`ticket ${u}: left as it is, not assigned to you`}const m=await p("gh",["issue","close",s,"--repo",r]);if(m.status!==0)throw new Error(m.stderr||"gh issue close failed");return`ticket ${u}: closed`}catch(s){return b(s)}}o(_,"moveTicket");export{Z as cardIdOf,_ as moveTicket,E as resolveTicket,C as ticketBadge,g as ticketIn,N as ticketSource,z as ticketTarget,R as titleTickets};
|
package/board/worktree.mjs
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)[0]}}
|
|
1
|
+
var D=Object.defineProperty;var c=(r,e)=>D(r,"name",{value:e,configurable:!0});import p from"node:fs";import k from"node:path";import E from"node:os";import{randomUUID as F}from"node:crypto";import{spawnSync as T}from"node:child_process";import{fileURLToPath as N}from"node:url";import{BoardError as A}from"./store.mjs";import{boardAt as O,hubCode as M,listBoards as P,readManifest as _}from"../hub/boards.mjs";import{boardRepo as q}from"../hub/board-repo.mjs";import{cloneRoot as B}from"../shared/repos.mjs";import{storedPath as j}from"../bin/install.mjs";import{hubService as K}from"../shared/service.mjs";const L=k.join(k.dirname(N(import.meta.url)),"..","bin","kickflip.mjs"),W=B,J=c(()=>k.join(E.homedir(),".claude","kickflip","worktrees"),"worktreesDir"),f=c(r=>{throw new A(r)},"fail");function a(r,...e){const t=T("git",["-C",r,"-c","core.commentChar=;",...e],{encoding:"utf8"});return{ok:t.status===0,out:(t.stdout||"").trim(),err:(t.stderr||t.error?.message||"").trim().split(`
|
|
2
|
+
`)[0]}}c(a,"git");const I=c((r,e)=>a(r,"rev-parse","--short",e).out,"short");function H(r,{repo:e=W(),base:t="main",remote:n=!1,busy:o=[]}={}){const s=k.resolve(e);a(s,"rev-parse","--git-dir").ok||f(`--repo ${s} is not a git repo`),a(s,"check-ref-format","--branch",r).ok||f(`--worktree: "${r}" can't be a branch name`),a(s,"check-ref-format","--branch",t).ok||f("--worktree: invalid base branch");const l=n?`refs/remotes/origin/${t}`:`refs/heads/${t}`;n&&a(s,"fetch","--quiet","--no-tags","origin",`+refs/heads/${t}:${l}`),a(s,"rev-parse","--verify","--quiet",l).ok||f(`--worktree: ${s} has no ${t} branch`);const u=k.join(J(),r),g=p.existsSync(u)?U(s,u,r,{ref:l,base:t,busy:o}):null;a(s,"rev-parse","--verify","--quiet",`refs/heads/${r}`).ok&&f(`--worktree: branch ${r} already exists in ${s}`),p.mkdirSync(k.dirname(u),{recursive:!0});const w=a(s,"worktree","add","--quiet","-b",r,u,l);return w.ok||f(`--worktree: git worktree add failed: ${w.err}`),{path:j(u),branch:r,repo:j(s),remade:g}}c(H,"makeWorktree");function U(r,e,t,{ref:n,base:o,busy:s}){const l=c(i=>f(`--worktree: ${e} ${i}; land or remove it first`),"refuse");s.includes(j(e))&&l("already exists and another open builder record uses it");const u=p.realpathSync(e);a(r,"worktree","list","--porcelain").out.split(`
|
|
3
|
+
|
|
4
|
+
`).some(i=>{const d=i.split(`
|
|
5
|
+
`),b=d.find(x=>x.startsWith("worktree "))?.slice(9);try{return d.includes(`branch refs/heads/${t}`)&&p.realpathSync(b)===u}catch{return!1}})||l("already exists");const w=Number(a(r,"rev-list","--count",`${n}..refs/heads/${t}`).out),h=a(e,"status","--porcelain").out.split(`
|
|
6
|
+
`).filter(Boolean).length,v=c((i,d)=>`${i} ${d}${i===1?"":"s"}`,"count"),S=[...w?[`${v(w,"commit")} beyond ${o}`]:[],...h?[v(h,"changed file")]:[]];S.length&&l(`holds ${S.join(" and ")}`);const y=a(r,"worktree","remove","--force",e);y.ok||f(`--worktree: could not remove the leftover ${e}: ${y.err}`);const $=a(r,"branch","--quiet","-D",t);return $.ok||f(`--worktree: could not delete the leftover branch ${t}: ${$.err}`),`Remade the leftover worktree ${j(e)} from ${o}: it held no commits or changes.`}c(U,"removeLeftover");function Y(r,{repo:e,manifest:t=_()}={}){const n=r?O(r,t):null;if(!n)return{repo:e??W(),base:"main",remote:!1};const o=q(n.route,t);return{repo:e??(o.repo||f(`/${n.route} has no folder recorded for its session; rejoin it from its repo folder`)),base:o.branch||"main",remote:!o.local}}c(Y,"builderRepo");const $t=c((r,e,{repo:t,busy:n}={})=>H(e,{...Y(r,{repo:t}),busy:n}),"boardWorktree");function X(r){const e=[],t=c(n=>{for(const o of p.readdirSync(n,{withFileTypes:!0}))o.name===".git"||o.name===".repos"||o.name==="node_modules"||(o.isDirectory()?t(k.join(n,o.name)):o.name.endsWith(".test.mjs")&&e.push(k.join(n,o.name)))},"walk");return t(r),e.sort()}c(X,"testFiles");function R(r){const e=X(r);if(!e.length)return{ok:!0,pass:0,fail:0};const{NODE_TEST_CONTEXT:t,...n}=process.env,o=T(process.execPath,["--test","--test-reporter=tap",...e],{cwd:r,env:n,encoding:"utf8",maxBuffer:256*1024*1024}),s=c(u=>Number((o.stdout||"").match(new RegExp(`^# ${u} (\\d+)$`,"m"))?.[1]??0),"count"),l={ok:o.status===0,pass:s("pass"),fail:s("fail")};if(!l.ok){const u=(o.stdout||"")+(o.stderr||"")+(o.error?`
|
|
3
7
|
${o.error.message}
|
|
4
|
-
`:""),
|
|
5
|
-
`)[0];
|
|
6
|
-
`),t=[],n=[],o=
|
|
7
|
-
`));if(
|
|
8
|
-
${
|
|
9
|
-
|
|
10
|
-
`):""}`)}return n}
|
|
11
|
-
`)[0]}
|
|
12
|
-
`).
|
|
13
|
-
`)),n(`
|
|
14
|
-
`)
|
|
8
|
+
`:""),g=k.join(E.homedir(),".claude","kickflip","logs");try{p.mkdirSync(g,{recursive:!0});const h=k.join(g,`land-${F()}.tap`);p.writeFileSync(h,u,{mode:384}),l.log=h}catch(h){l.logError=`Could not save the full test output: ${h.message}`}l.failures=z(o.stdout||"");const w=(o.stderr||"").trim().split(`
|
|
9
|
+
`)[0];l.error=o.error?.message||`${o.signal?`test runner killed by ${o.signal}`:`test runner exited ${o.status}`}${w?`: ${w}`:""}`}return l}c(R,"nodeTests");function z(r){const e=r.split(`
|
|
10
|
+
`),t=[],n=[],o=c(s=>s.startsWith("'")?s.slice(1,-1).replaceAll("''","'"):s.startsWith('"')?JSON.parse(s):s,"scalar");for(let s=0;s<e.length;s++){const l=/^(\s*)# Subtest: (.*)$/.exec(e[s]);if(l){const i=l[1].length;for(;t.length&&t.at(-1).indent>=i;)t.pop();t.push({indent:i,name:l[2]})}const u=/^(\s*)not ok \d+ - (.*)$/.exec(e[s]);if(!u||/ # (?:TODO|SKIP)\b/i.test(u[2]))continue;const g=u[1].length,w=[...t.filter(i=>i.indent<g).map(i=>i.name),u[2]].join(" > "),h=[];for(let i=s+1;i<e.length&&e[i].startsWith(" ".repeat(g+2));i++)h.push(e[i]);const v=c(i=>h.find(d=>d.startsWith(`${" ".repeat(g+2)}${i}: `))?.slice(g+i.length+4),"field"),S=v("location"),y=v("error");let $=[];if(y&&/^[|>]/.test(y)){const i=h.findIndex(d=>d.startsWith(`${" ".repeat(g+2)}error: `))+1;for(let d=i;d<h.length&&h[d].startsWith(" ".repeat(g+4));d++)$.push(h[d].slice(g+4))}else y&&($=o(y).split(`
|
|
11
|
+
`));if($[0]==="test failed"){const i=[];let d=s-1;for(e[d]?.startsWith("# Subtest: ")&&d--;d>=0&&e[d].startsWith("# ")&&!e[d].startsWith("# Subtest: ");d--)i.unshift(e[d].slice(2));const b=i.findIndex(x=>/^\w*Error(?: \[[^\]]+\])?:/.test(x));b>=0&&($=i.slice(b))}n.push(`${w}
|
|
12
|
+
${S?o(S):"file not reported by test runner"}${$.length?`
|
|
13
|
+
`+$.slice(0,3).map(i=>` ${i}`).join(`
|
|
14
|
+
`):""}`)}return n}c(z,"tapFailures");function G(r,e){let t;if(process.env.KICKFLIP_RESTART_CMD){try{t=JSON.parse(process.env.KICKFLIP_RESTART_CMD)}catch{}(!Array.isArray(t)||!t.length||!t.every(o=>typeof o=="string"))&&f("KICKFLIP_RESTART_CMD must be a JSON array of strings"),t=[...t,r,...e?[e]:[]]}else{if(r==="hub")return K().restart();t=[process.execPath,L,"restart",e]}const n=T(t[0],t.slice(1),{encoding:"utf8"});return n.status===0?null:(n.stderr||n.error?.message||`exit ${n.status}`).trim().split(`
|
|
15
|
+
`)[0]}c(G,"restartRunner");const Q=c(()=>k.join(E.homedir(),".claude","kickflip","land.lock"),"landLock"),V=c(r=>{try{return process.kill(r,0),!0}catch(e){return e.code==="EPERM"}},"alive");async function pt(r,e){const t=Q();for(p.mkdirSync(k.dirname(t),{recursive:!0});;)try{p.writeFileSync(t,JSON.stringify({card:r,pid:process.pid,at:new Date().toISOString()}),{flag:"wx"});break}catch(n){if(n.code!=="EEXIST")throw n;let o=null;try{o=JSON.parse(p.readFileSync(t,"utf8"))}catch{}Number.isInteger(o?.pid)&&V(o.pid)&&f(`${o.card} is landing (since ${o.at}); land again once it finishes`),p.rmSync(t,{force:!0})}try{return await e()}finally{try{JSON.parse(p.readFileSync(t,"utf8")).pid===process.pid&&p.rmSync(t,{force:!0})}catch{}}}c(pt,"holdingLandLock");function Z(r,e,t){const o=a(r,"diff","--name-only","--no-renames","--diff-filter=A",e,t,"--","docs/adr").out.split(`
|
|
16
|
+
`).find(s=>/^docs\/adr\/\d{4}-(?!\d{2}-\d{2}-\d{4}-)/.test(s));o&&f(`land: ${o} is named with a number; name it docs/adr/YYYY-MM-DD-HHMM-${k.basename(o).replace(/^\d{4}-/,"")}, the time you wrote it, and land again`)}c(Z,"refuseNumberedAdr");function tt(r,e,t,n){const o=a(r,"log","--format=%h",`${e}..${t}`).out.split(`
|
|
17
|
+
`).filter(Boolean);for(const s of o){const l=a(r,"log","-1","--format=%s",s).out;n&&!l.startsWith(`${n}: `)&&f(`land: commit ${s} "${l}" doesn't start with "${n}: "; reword it and land again`),/^# Conflicts:/m.test(a(r,"log","-1","--format=%B",s).out)&&f(`land: commit ${s} "${l}" still holds a "# Conflicts:" note in its message; amend it out and land again`)}}c(tt,"refuseBadMessages");function et(r,{say:e=console.log,runTests:t=R,outcome:n}){const o=t(r);o.ok||f([`tests failed in ${r} (${o.fail} failing); ${n}`,...o.failures?.length?o.failures:[o.error||"test runner failed without test diagnostics"],...o.log?[`Full test output: ${o.log}`]:o.logError?[o.logError]:[]].join(`
|
|
18
|
+
`)),e(`tests passed: ${o.pass}`)}c(et,"passTests");function gt({path:r,branch:e,repo:t,base:n,ref:o},{say:s=console.log,runTests:l=R,restart:u=G,boards:g=c(()=>P(),"boards"),clone:w=W()}={}){p.existsSync(r)||f(`no worktree at ${r}`);const h=a(t,"symbolic-ref","--quiet","--short","HEAD").out;if(h!==n&&f(`${t} is on ${h||"a detached HEAD"}, not ${n}; switch it to ${n} (git -C ${t} switch ${n}) and land again`),a(r,"status","--porcelain").out&&f(`${r} has uncommitted changes; commit them there first`),Number(a(t,"rev-list","--count",`${n}..${e}`).out)===0&&f(`${e} has no commits beyond ${n}; nothing to land`),!a(t,"merge-base","--is-ancestor",n,e).ok){if(!a(r,"rebase","--quiet",n).ok){const m=a(r,"diff","--name-only","--diff-filter=U").out.split(`
|
|
19
|
+
`).filter(Boolean);a(r,"rebase","--abort"),f(`rebasing ${e} onto ${n} conflicts in ${m.join(", ")||"files git did not name"}; resolve it in ${r} and land again`)}s(`rebased ${e} onto ${n} (${I(t,n)})`)}const v=a(t,"log","--format=%h %s",`${n}..${e}`).out.split(`
|
|
20
|
+
`).filter(m=>m&&!m.includes(" "));v.length&&f(`${e} has a commit with an empty subject (${v[0]}); reword it in ${r} and land again`),Z(t,n,e),tt(t,n,e,o),et(r,{say:s,runTests:l,outcome:"nothing landed"});const S=a(t,"diff","--name-only",n,e).out.split(`
|
|
21
|
+
`).filter(Boolean),y=a(t,"merge","--quiet","--ff-only",e);y.ok||f(`could not fast-forward ${n} in ${t}: ${y.err}`),s(`${n} fast-forwarded to ${I(t,n)} in ${t}`);const $=[],i=p.realpathSync(t)===p.realpathSync(w)?S.filter(m=>!/\.test\.mjs$/.test(m)):[],d=new Set(i.length?M(k.join(t,"hub","hub.mjs")).files:[]);if(i.some(m=>m.startsWith("hub/")||d.has(m))){const m=u("hub");m?$.push(`the hub didn't restart (${m})`):s("hub restarted, with every site")}else if(i.some(m=>m.startsWith("board/")))for(const m of g()){const C=u("board",m.route);C?$.push(`/${m.route} didn't restart (${C})`):s(`board restarted: ${m.route}`)}const b=a(t,"worktree","remove",r);b.ok?s(`worktree removed: ${r}`):$.push(`could not remove ${r} (${b.err})`);const x=a(t,"branch","--quiet","-d",e);x.ok?s(`branch deleted: ${e}`):$.push(`could not delete branch ${e} (${x.err})`),$.length&&f(`landed, but ${$.join("; ")}`)}c(gt,"land");export{$t as boardWorktree,Y as builderRepo,a as git,pt as holdingLandLock,gt as land,H as makeWorktree,R as nodeTests,et as passTests,G as restartRunner,W as skillRepo,J as worktreesDir};
|