@lovek4d/kickflip 0.3.1 → 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.
Files changed (104) hide show
  1. package/CONTEXT.md +151 -115
  2. package/README.md +23 -14
  3. package/SKILL.md +37 -37
  4. package/bin/doctor.mjs +4 -4
  5. package/bin/install.mjs +3 -3
  6. package/bin/kickflip.mjs +34 -26
  7. package/bin/wait-events.mjs +2 -2
  8. package/board/arch-review.mjs +1 -1
  9. package/board/auto-review.mjs +2 -0
  10. package/board/bin/board.mjs +186 -69
  11. package/board/domain.mjs +8 -5
  12. package/board/finish.mjs +3 -0
  13. package/board/headless.mjs +3 -0
  14. package/board/on-host.mjs +1 -1
  15. package/board/permissions.mjs +2 -0
  16. package/board/public/app-arch.js +2 -2
  17. package/board/public/app-asks.js +57 -38
  18. package/board/public/app-banner.js +2 -2
  19. package/board/public/app-cards.js +156 -72
  20. package/board/public/app-core.js +2 -4
  21. package/board/public/app-header.js +12 -5
  22. package/board/public/app-lanes.js +201 -47
  23. package/board/public/app-needs.js +2 -1
  24. package/board/public/app-snapshot.js +2 -2
  25. package/board/public/app-threads.js +11 -14
  26. package/board/public/app.html +2 -1
  27. package/board/public/app.js +53 -83
  28. package/board/public/attach.js +39 -3
  29. package/board/public/dialogs.js +59 -14
  30. package/board/public/hub-kit.js +9 -6
  31. package/board/public/hub.css +5 -18
  32. package/board/public/linear-key.js +6 -4
  33. package/board/public/log.html +48 -0
  34. package/board/public/prompts.js +9 -8
  35. package/board/public/prototype-kit.js +6 -6
  36. package/board/public/prototype-switcher.js +13 -5
  37. package/board/public/prototype.html +1 -0
  38. package/board/public/router.js +8 -7
  39. package/board/public/rules-editor.js +3 -4
  40. package/board/public/shell.js +20 -6
  41. package/board/public/sidebar.css +21 -1
  42. package/board/public/sidebar.js +64 -31
  43. package/board/public/style.css +155 -62
  44. package/board/public/toasts.js +2 -2
  45. package/board/public/ui-cards.js +176 -71
  46. package/board/public/ui-enums.js +5 -4
  47. package/board/public/ui-header.js +10 -10
  48. package/board/public/ui-hub.js +4 -4
  49. package/board/public/ui-markdown.js +1 -1
  50. package/board/public/ui-order.js +9 -6
  51. package/board/public/ui-page-header.js +10 -7
  52. package/board/public/ui-patch.js +53 -0
  53. package/board/public/ui-phase-bars.js +17 -0
  54. package/board/public/ui-prompts.js +8 -8
  55. package/board/public/ui-questions.js +26 -23
  56. package/board/public/ui-sidebar.js +46 -22
  57. package/board/public/ui-snapshot.js +20 -9
  58. package/board/public/ui-threads.js +17 -11
  59. package/board/public/ui.js +80 -12
  60. package/board/public/view-frame.js +1 -1
  61. package/board/public/view-hub-settings.js +25 -18
  62. package/board/public/view-hub.js +53 -12
  63. package/board/public/view-notes.js +1 -1
  64. package/board/public/view-questions.js +101 -50
  65. package/board/public/view-settings.js +167 -49
  66. package/board/review.mjs +1 -1
  67. package/board/server.mjs +18 -12
  68. package/board/store.mjs +13 -12
  69. package/board/tickets.mjs +3 -3
  70. package/board/worktree.mjs +20 -14
  71. package/docs/hub-session.md +4 -4
  72. package/hooks-plugin/hooks/codex-share-auto.mjs +1 -1
  73. package/hooks-plugin/hooks/watcher-stop.mjs +2 -2
  74. package/hub/board-repo.mjs +1 -1
  75. package/hub/boards.mjs +4 -4
  76. package/hub/close-board.mjs +1 -1
  77. package/hub/github.mjs +4 -4
  78. package/hub/home.mjs +1 -1
  79. package/hub/hub.mjs +12 -12
  80. package/hub/incoming.mjs +2 -0
  81. package/hub/linear.mjs +2 -2
  82. package/hub/live.mjs +2 -2
  83. package/hub/new-board.mjs +2 -2
  84. package/hub/permission-denied-hook.mjs +2 -0
  85. package/hub/pr-report.mjs +3 -1
  86. package/hub/presence.mjs +1 -1
  87. package/hub/prompts.mjs +1 -1
  88. package/hub/push.mjs +2 -2
  89. package/hub/readiness.mjs +2 -2
  90. package/hub/repos.mjs +3 -3
  91. package/hub/restart-board.mjs +1 -1
  92. package/hub/schedule.mjs +1 -1
  93. package/hub/sessions.mjs +2 -2
  94. package/hub/sw.js +1 -1
  95. package/hub/try.mjs +8 -4
  96. package/hub/usage.mjs +3 -3
  97. package/hub/views.mjs +1 -1
  98. package/package.json +5 -1
  99. package/shared/card-prs.mjs +1 -0
  100. package/shared/colors.css +5 -4
  101. package/shared/fake-gh.mjs +59 -8
  102. package/shared/repos.mjs +1 -1
  103. package/shared/usage-gate.mjs +1 -1
  104. package/board/public/view-polling.js +0 -87
package/board/server.mjs CHANGED
@@ -1,17 +1,23 @@
1
1
  #!/usr/bin/env node
2
- var _t=Object.defineProperty;var c=(t,e)=>_t(t,"name",{value:e,configurable:!0});import{topicItems as Dt}from"./review.mjs";import{boardWorktree as lt}from"./worktree.mjs";import{fromHost as Ft}from"./on-host.mjs";import{archOf as Ht,readReviews as Bt,repoName as Ut,startReview as Ct}from"./arch-review.mjs";import Jt from"node:http";import y from"node:fs/promises";import p from"node:path";import{execFile as ut}from"node:child_process";import{fileURLToPath as Mt}from"node:url";import dt from"node:vm";import Lt from"node:zlib";import Wt from"node:crypto";import{promisify as Gt}from"node:util";import{buildBoard as A,ITEM_EVENTS as zt,needsAnswer as Qt,ownerActions as Vt,pullOf as Xt,refOf as X,startedByAnswer as Kt,threadOf as Yt,todoOf as Zt}from"./domain.mjs";import{localDay as qt}from"../hub/schedule.mjs";import{dropQueued as te}from"../hub/push.mjs";import{createReadStream as ee,existsSync as ae,readdirSync as ne,statSync as ie,watch as re}from"node:fs";import{BoardError as P,addTopic as se,boardConfig as oe,dueQueuedBuilds as ce,migrateFollowups as le,pickUp as ue,startQueuedBuild as de,startTappedBuild as fe,sendPush as pe,recordOwnerTap as me,readPrototypes as we,takeFile as he,finishHandshake as ge,itemSources as J,readAskLinks as K,readAskProgress as ye,readBoardRules as ke,readBoardSettings as Se,readHandshake as be,readHidden as ft,readLineupNotes as ve,readNotes as je,readWorking as Y,readUpload as Oe,resolveNoteAnswers as Pe,saveBoardRules as Ee,saveBoardSettings as Te,saveUpload as $e,setHidden as Ae,settingsOf as x,threadFile as xe,wavesDir as Re}from"./store.mjs";import{changed as Ie,readHubRules as Ne}from"../hub/rules.mjs";import{boardAt as Z,listenState as _e,linksOf as De}from"../hub/boards.mjs";import{pendingFor as Fe,shown as He}from"../hub/prompts.mjs";import{routeRecord as E}from"../hub/routes.mjs";import{FOREVER as Be,durationEnd as Ue}from"../hub/duration.mjs";import{durationChoices as Ce}from"../hub/views.mjs";import{VIEW_PAGES as Je,shellHtml as Me}from"../hub/shell.mjs";import{crossSite as Le,isJSON as We}from"../starter/cross-site.mjs";import{readUsageGate as pt}from"../shared/usage-gate.mjs";import{reviewWaits as Ge}from"../shared/skills.mjs";import{boardRepo as ze}from"../hub/board-repo.mjs";const N=p.dirname(Mt(import.meta.url));async function mt(){const t=await y.stat(p.join(N,"..","hub","icon-180.png")).catch(()=>null);return`/_hub/icon-180.png?v=${t?Math.floor(t.mtimeMs).toString(36):"0"}`}c(mt,"touchIcon");async function Qe(t){if(!t.includes("<!--header "))return t;const e=dt.createContext({});return dt.runInContext(await y.readFile(p.join(N,"public","ui-page-header.js"),"utf8"),e),t.replace(/<!--header (\{.*?\})-->/g,(n,a)=>e.BoardUI.header(JSON.parse(a)))}c(Qe,"pageHeaders");const d=p.resolve(process.argv[2]||process.cwd()),Ve=[p.join(d,"public"),p.join(N,"public"),p.join(N,"..","starter","public"),p.join(N,"..","shared")],b=p.join(d,"data"),F=p.join(b,"events.jsonl"),M=p.join(b,"replies"),wt=Number(process.env.PORT||4600),R=(process.env.PREFIX||"").replace(/\/+$/,""),ht={".html":"text/html; charset=utf-8",".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".txt":"text/plain; charset=utf-8",".md":"text/markdown; charset=utf-8",".plist":"text/xml; charset=utf-8",".ipa":"application/octet-stream",".apk":"application/vnd.android.package-archive"},Xe=Gt(Lt.gzip),Ke=/^(text\/|application\/json|image\/svg\+xml)/;async function gt(t,e,n,a){const i={...n};let r=Buffer.isBuffer(a)?a:Buffer.from(a??"");if(Ke.test(i["content-type"]||"")&&(i.vary="accept-encoding",r.length>=1024&&/\bgzip\b/.test(t.req?.headers["accept-encoding"]||"")))try{r=await Xe(r),i["content-encoding"]="gzip"}catch{}i["content-length"]=r.length,t.writeHead(e,i),t.end(t.req?.method==="HEAD"?void 0:r)}c(gt,"send");const g=c((t,e,n)=>gt(t,e,{"content-type":ht[".json"],"cache-control":"no-store"},JSON.stringify(n)),"json"),L=c(t=>`${Math.floor(t.mtimeMs).toString(36)}-${t.size.toString(36)}`,"versionOf"),yt=/\b(src|href)="([\w.][\w./-]*\.(?:js|css))"/g;async function Ye(t,e){const n=e.endsWith("/")?e:p.posix.dirname(e),a=[...new Set([...t.matchAll(yt)].map(r=>r[2]))],i=new Map(await Promise.all(a.map(async r=>{const s=await Ot(p.posix.join(n,r)),o=s?await y.stat(s).catch(()=>null):null;return[r,o?L(o):null]})));return t.replace(yt,(r,s,o)=>i.get(o)?`${s}="${o}?v=${i.get(o)}"`:r)}c(Ye,"versionAssets");const Ze=c((t,e)=>String(t||"").split(",").map(n=>n.trim().replace(/^W\//,"")).includes(e.replace(/^W\//,"")),"sameTag"),v=c(async(t,e)=>{try{return JSON.parse(await y.readFile(t,"utf8"))}catch{return e}},"readJSON"),I=c(async t=>{try{return(await y.readFile(t,"utf8")).split(`
3
- `).filter(Boolean).map(e=>JSON.parse(e))}catch{return[]}},"readLines"),H=c(t=>xe(d,t),"threadFile"),W=c(async()=>{const t=await ft(d);return(await I(F)).filter(e=>!t.has(e.id))},"shownEvents");async function _(t,e=64*1024){if(!We(t.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});let n="";for await(const a of t)if(n+=a,n.length>e)throw Object.assign(new Error("too large"),{status:413});return n?JSON.parse(n):{}}c(_,"readBody");const O=c(async(t,e)=>(await q([[t,e]]))[0],"appendEvent");async function q(t){const e=new Date().toISOString(),n=t.map(([a,i])=>({id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,7),ts:e,kind:a,data:i}));return await y.mkdir(M,{recursive:!0}),await y.appendFile(F,n.map(a=>JSON.stringify(a)+`
4
- `).join("")),n.map(a=>a.id)}c(q,"appendEvents");async function kt(t){const e=await q(t.map(i=>["owner-answer",i]));await Pe(d,t);const n=A(await Q({github:!1})),a=[...new Set(t.map(i=>Kt(n,String(i.id))).filter(Boolean))];return a.length&&await q(a.map(i=>["card-unblocked",{key:i}])),e}c(kt,"recordAnswers");async function qe(t){if(!Array.isArray(t)||!t.length)return"Send at least one answer.";const e=new Set(((await v(p.join(b,"owner.json"),{items:[]})).items||[]).map(a=>a.id)),n=t.filter(a=>!e.has(String(a?.id)));return n.length?`${n.map(a=>String(a?.id)).join(", ")} ${n.length===1?"is":"are"} no longer asked here. Reload to see what is, then send again.`:null}c(qe,"answersRefused");async function G(t){for(const e of["html","json"]){const n=p.join(M,`${t.id}.${e}`);try{const[a,i]=await Promise.all([y.readFile(n,"utf8"),y.stat(n)]);return{...t,reply:{[e]:e==="json"?JSON.parse(a):a,at:i.mtime.toISOString()}}}catch{}}return{...t,reply:null}}c(G,"withReply");async function ta(t,e){const n=await Y(d);let a=await W();return t&&(a=a.filter(i=>i.kind===t)),Promise.all(a.reverse().slice(0,e).map(async i=>{const r=await G(i);return r.reply||!E(n,i.id)?r:{...r,working:E(n,i.id)}}))}c(ta,"listEvents");const St=new Map,ea="number,title,url,state,isDraft,reviewDecision,mergeStateStatus,mergeable,statusCheckRollup,mergedAt,headRefName,headRefOid,updatedAt,latestReviews";function aa(t,e){return new Promise(n=>{ut("gh",["pr","view",String(e),"--repo",t,"--json",ea],{timeout:2e4},(a,i)=>{if(a)return n(null);try{n(JSON.parse(i))}catch{n(null)}})})}c(aa,"ghPR");function na(t,e){return new Promise(n=>{ut("gh",["api",`repos/${t}/pulls/${e}/comments?per_page=100`],{timeout:2e4},(a,i)=>{if(a)return n(0);try{const r=JSON.parse(i),s=new Set(r.map(o=>o.in_reply_to_id).filter(Boolean));n(r.filter(o=>!o.in_reply_to_id&&/codex/i.test(o.user?.login||"")&&!s.has(o.id)).length)}catch{n(0)}})})}c(na,"ghCodexOpen");function ia(t){const e=t||[];let n=0,a=0;for(const i of e){const r=(i.conclusion||i.state||"").toUpperCase(),s=(i.status||"").toUpperCase();["FAILURE","ERROR","CANCELLED","TIMED_OUT","ACTION_REQUIRED","STARTUP_FAILURE"].includes(r)?n++:(s&&s!=="COMPLETED"||r==="PENDING"||r==="EXPECTED")&&a++}return{total:e.length,fail:n,pending:a,state:n?"fail":a?"pending":e.length?"pass":"none"}}c(ia,"checksOf");async function ra(t,e){const n=`${t}#${e}`;let a=St.get(n);return a||(a={at:0,value:null,loading:null},St.set(n,a)),Date.now()-a.at>45e3&&!a.loading&&(a.loading=Promise.all([aa(t,e),na(t,e)]).then(([i,r])=>{if(i){const{statusCheckRollup:s,latestReviews:o,...l}=i,m=Object.fromEntries((o||[]).map(h=>[h.author?.login,h.state]));a.value={...l,checks:ia(s),reviews:m,codexOpen:r}}a.at=Date.now(),a.loading=null})),!a.value&&a.loading&&await a.loading,a.value}c(ra,"prState");const T=c(()=>oe(d),"config");async function sa(){const t=await y.readdir(M).catch(()=>[]),e=await Promise.all(t.map(a=>y.stat(p.join(M,a)).then(i=>i.mtimeMs,()=>0))),n=Math.max(0,...e);return n?new Date(n).toISOString():null}c(sa,"latestReply");async function bt(t){let e=[];try{e=(await y.readdir(t)).filter(i=>i.endsWith(".json"))}catch{}const n=await Promise.all(e.map(i=>v(p.join(t,i),null))),a=Object.create(null);return e.forEach((i,r)=>{if(!n[r])return;const[s,o="main"]=p.basename(i,".json").split("@");(a[s]||=Object.create(null))[o]=n[r]}),a}c(bt,"readAgents");async function z(){const t=await T(),e=t.items==="waves",n=await J(d),a=(await Promise.all(n.map(async s=>{const o=await v(s.file,null);return!o||!Array.isArray(o.items)?null:{slug:s.slug,wave:o,agents:await bt(p.join(s.dir,"agents"))}}))).filter(Boolean),i=Object.create(null);await Promise.all(a.flatMap(({slug:s,wave:o})=>o.items.map(async l=>{i[`${s}/${l.id}`]=await I(H(`${s}/${l.id}`))})));const r=Z(d);return{github:r?.github,remote:r?.remote,branch:r?.branch,reviewers:t.reviewers||[],fromWaves:e,autoMerge:x(t).auto_merge,autoBuild:x(t).auto_build,tickets:x(t).tickets,waves:a,threads:i,working:await Y(d)}}c(z,"itemSnapshot");async function Q({github:t=!0}={}){const e=await z(),{waves:n,threads:a,working:i}=e,r=Object.create(null);t&&await Promise.all(n.flatMap(({wave:l})=>{const m=e.github||l.repo;return m?l.items.filter(h=>h.pr).map(async h=>{r[`${m}#${h.pr}`]=await ra(m,h.pr)}):[]}));const s=new Set(["ask","owner-answer","grill-more","item-message","wave-triage","todo-triage"]),o=await Promise.all((await W()).map(l=>s.has(l.kind)||E(i,l.id)?G(l):{...l,reply:null}));return await Promise.all(o.filter(l=>l.kind==="ask").map(async l=>{const m=await I(H(`ask:${l.id}`));m.length&&(a[`ask:${l.id}`]=m)})),{...e,route:R.slice(1)||p.basename(d),try:await v(p.join(b,"try.json"),null),main:await v(p.join(b,"main.json"),null),prs:r,events:o,askLinks:await K(d),askProgress:await ye(d),checked:await v(p.join(b,"checked.json"),null),watcher:await v(p.join(b,"watcher.json"),null),replyAt:await sa(),owner:await v(p.join(b,"owner.json"),{items:[]}),topicItems:await Dt()}}c(Q,"snapshot");let tt=!1;async function et(){if(!tt){tt=!0;try{const t=p.join(b,"builder-stalled.json"),e=await v(t,{}),n=Object.create(null);let a;for(const{key:i,lane:r,builder:s}of A(await z()).items)r==="done"||!s?.stalled||(n[i]=s.silentSince,E(e,i)!==s.silentSince&&(a||=await I(F),a.some(o=>o.kind==="builder-stalled"&&o.data?.key===i&&o.data?.silentSince===s.silentSince)||await O("builder-stalled",{key:i,silentSince:s.silentSince})));JSON.stringify(n)!==JSON.stringify(e)&&(await y.mkdir(b,{recursive:!0}),await y.writeFile(t+".tmp",JSON.stringify(n)+`
5
- `),await y.rename(t+".tmp",t))}catch(t){console.error("Builder check failed:",t.message)}finally{tt=!1}}}c(et,"checkStalled");async function oa(t){const e=await be(d);if(!e?.hello?.id||e.done_at)return null;const n=t?.find(i=>i.link==="github"),a={events:e.ack?.id===e.hello.id,...n||!t?{github:!!n?.ok}:{}};return a.events&&a.github!==!1?(await ge(d),null):a}c(oa,"setupOf");async function $(){const t=new Date().toISOString(),{title:e,reviewers:n}=await T(),a=Z(d),i=a?_e(a):"deaf",r=A({...await Q(),listen:i,prompts:a?Fe(a.route).map(He):[]});for(const o of r.items)o.worktree&&!ae(o.worktree)&&(o.worktree=null);const s=a?De(a,{listen:i}):null;return{generatedAt:t,title:e,reviewers:(n||[]).map(o=>o?.name).filter(o=>typeof o=="string"),...r,links:s,setup:await oa(s),holdChoices:Ce(new Date,"until you unhold it"),arch:a?Ht(await Bt(d),r.items,r.owner,{repo:Ut(ze(a.route)),waits:Ge()}):null}}c($,"board");let vt=Promise.resolve();function ca(){const t=vt.then(async()=>{const e=Z(d);if(!e)throw Object.assign(new P("This board isn\u2019t in the hub\u2019s manifest."),{status:404});const{arch:n}=await $();if(n.last?.open)return{key:n.last.key,ref:n.last.ref,existing:!0};const{key:a,data:i}=await Ct(d,e),r=await O("arch-review",i);return{key:a,id:r,existing:!1}});return vt=t.catch(()=>{}),t}c(ca,"runReview");async function la(t){const[e,n]=t.split("/"),a=(await J(d)).find(s=>s.slug===e),i=a?await v(a.file,null):null,r=Array.isArray(i?.items)&&i.items.find(s=>s.id===n)||null;return{item:r,agents:r?E(await bt(p.join(a.dir,"agents")),n)||{}:{}}}c(la,"itemOf");async function ua(t){const e=E(await K(d),t),{tickets:n}=x(await T()),a=[];for(const i of await J(d)){const r=await v(i.file,null);for(const s of Array.isArray(r?.items)?r.items:[]){const o=`${i.slug}/${s.id}`;s.from_ask===t?a.unshift({key:o,ref:X(r,s,n),became:!0}):o===e&&a.push({key:o,ref:X(r,s,n),became:!1})}}return a}c(ua,"askLinksOf");async function da(t){const e=await v(p.join(b,"owner.json"),{items:[]}),n=new Set((e.items||[]).filter(w=>w.topic===`item:${t}`).map(w=>w.id)),{item:a,agents:i}=t.startsWith("ask:")?{item:null,agents:{}}:await la(t),r=t.startsWith("ask:"),s=r?t.slice(4):a?.from_ask||null,o=r?[]:Object.entries(await K(d)).filter(([,w])=>w===t).map(([w])=>w),l=[s,...o].filter(Boolean),m=new Set([t,...l.map(w=>`ask:${w}`)]),h=(await W()).filter(w=>m.has(w.data?.key)&&zt.includes(w.kind)||w.kind==="owner-answer"&&n.has(w.data?.id)||w.kind==="ask"&&l.includes(w.id)||w.kind==="ask-edited"),j=(await Promise.all([...m].map(w=>I(H(w))))).flat();return Yt(t,{lines:j,events:await Promise.all(h.map(G)),owner:e,working:await Y(d),fromAsk:s,links:r?await ua(s):[],agents:i,moved:o})}c(da,"thread");const jt=c(t=>String(t).replace(/[&<>"']/g,e=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[e]),"esc");async function Ot(t){for(const e of Ve){let n=p.resolve(e,"."+t);if(n!==e&&!n.startsWith(e+p.sep))return null;if((await y.stat(n).catch(()=>null))?.isDirectory()&&(n=p.join(n,"index.html")),await y.stat(n).then(i=>i.isFile(),()=>!1))return n}return null}c(Ot,"staticFile");const Pt=p.join(N,"public","prototype.html");async function fa(t){const e=/^\/prototype-([a-z0-9-]+)\.html$/.exec(t)?.[1];return e&&(await we(d)).find(n=>n.topic===e)||null}c(fa,"recordedPrototype");const Et=c(t=>JSON.stringify(t).replace(/</g,"\\u003c").replace(/\u2028/g,"\\u2028").replace(/\u2029/g,"\\u2029"),"scriptJSON");async function pa(t){const e=await Promise.all(t.takes.map(async r=>{const s=`prototypes/${t.topic}/${r.key}.js`,o=await y.stat(he(d,t.topic,r.key)).catch(()=>null);return{key:r.key,label:r.label,src:o?.isFile()?`${s}?v=${L(o)}`:null,file:s}})),n=`${t.title} \xB7 prototype`,a={lead:"back",backHref:t.card?`./#item=${encodeURIComponent(t.card)}`:"./",title:n,controls:!1},i={prototypeTitle:jt(n),prototypeHeader:`<!--header ${Et(a).replace(/>/g,"\\u003e")}-->`,prototypeData:Et({topic:t.topic,title:t.title,takes:e})};return(await y.readFile(Pt,"utf8")).replace(/\{\{(prototypeTitle|prototypeHeader|prototypeData)\}\}/g,(r,s)=>i[s])}c(pa,"prototypePage");async function ma(t,e){try{const n=decodeURIComponent(String(t.headers["x-filename"]||""));return g(e,200,await $e(d,t,{name:n,type:t.headers["content-type"],length:Number(t.headers["content-length"])||0}))}catch(n){if(!(n instanceof P)&&!(n instanceof URIError))throw n;return t.resume(),g(e,n.status||400,{error:n.message})}}c(ma,"upload");const wa=/^(text\/html|image\/svg\+xml|application\/(xhtml\+)?xml|text\/xml)$/;async function ha(t,e,n){const a=await Oe(d,n);if(!a){e.writeHead(404).end();return}if(e.writeHead(200,{"content-type":a.type.startsWith("text/")?`${a.type}; charset=utf-8`:a.type,"content-length":a.size,"content-disposition":`inline; filename*=UTF-8''${encodeURIComponent(a.name)}`,"cache-control":"private, max-age=86400","x-content-type-options":"nosniff",...wa.test(a.type)?{"content-security-policy":"sandbox"}:{}}),t.method==="HEAD"){e.end();return}ee(a.path).pipe(e)}c(ha,"sendUpload");const at=new Set;function ga(t,e){e.writeHead(200,{"content-type":"text/event-stream; charset=utf-8","cache-control":"no-store",connection:"keep-alive"}),e.write(`retry: 3000
2
+ var je=Object.defineProperty;var u=(t,e)=>je(t,"name",{value:e,configurable:!0});import{topicItems as Pe}from"./review.mjs";import{boardWorktree as ut}from"./worktree.mjs";import{fromHost as Ee,fromOwner as Te}from"./on-host.mjs";import{archOf as Ae,readReviews as $e,repoName as xe,startReview as Ne}from"./arch-review.mjs";import _e from"node:http";import m from"node:fs/promises";import Bt from"node:os";import f from"node:path";import{execFile as Ht}from"node:child_process";import{fileURLToPath as Re}from"node:url";import Y from"node:vm";import Ie from"node:zlib";import Ct from"node:crypto";import{promisify as De}from"node:util";import{archivedOf as Ft,buildBoard as dt,withHub as Be,DONE_KEPT as Jt,idleStep as He,idleSummary as Ce,ITEM_EVENTS as Fe,lineupMoved as Je,slimDone as Ut,needsAnswer as Ue,ownerActions as Me,picksNoTake as Le,pullOf as We,refOf as ft,startedByAnswer as Ge,strictlyIdle as ze,threadOf as Ke,triageOf as Qe}from"./domain.mjs";import{localDay as Ve,quietMiddleOf as Xe,quietOf as Ye}from"../hub/schedule.mjs";import{cardPRs as Ze,prKeyOf as Mt}from"../shared/card-prs.mjs";import{dropQueued as qe}from"../hub/push.mjs";import{createReadStream as ta,existsSync as Lt,readdirSync as ea,statSync as aa,watch as Wt}from"node:fs";import{launchBuilder as ht,reviveBuilders as na}from"./headless.mjs";import{BoardError as E,addTopic as ia,appendThread as Z,archiveCards as ra,archiveCount as sa,archiveRank as oa,readArchive as Gt,boardConfig as ca,changeLineup as la,landable as ua,migrateFollowups as da,stripDeadFields as fa,pickUp as ha,startQueuedBuild as wa,startTappedBuild as zt,sendPush as Kt,recordOwnerTap as ma,readPrototypes as pa,takeFile as ga,finishHandshake as ya,itemSources as q,readAskLinks as wt,readAskProgress as ka,readBoardRules as Sa,readBoardSettings as va,readHandshake as ba,readHidden as Oa,readLineupNotes as ja,readNotes as Pa,readWorking as mt,readUpload as Ea,removeReviewers as Ta,resolveNoteAnswers as Aa,resolveRequestedQuestion as $a,saveBoardRules as xa,saveBoardSettings as Na,saveUpload as _a,setHidden as Qt,settingsOf as T,threadFile as Ra,wavesDir as Ia}from"./store.mjs";import{changed as Da,readHubRules as Ba}from"../hub/rules.mjs";import{boardAt as tt,listenState as Ha,linksOf as Ca}from"../hub/boards.mjs";import{pendingFor as Fa,shown as Ja}from"../hub/prompts.mjs";import{routeRecord as _}from"../hub/routes.mjs";import{CHOICES as Ua,FOREVER as Vt,durationEnd as Xt}from"../hub/duration.mjs";import{VIEW_PAGES as Ma,shellHtml as La}from"../hub/shell.mjs";import{crossSite as Wa,isJSON as Ga}from"../starter/cross-site.mjs";import{readUsageGate as et}from"../shared/usage-gate.mjs";import{reviewWaits as za}from"../shared/skills.mjs";import{boardRepo as Yt}from"../hub/board-repo.mjs";import{permissionsView as Ka,setPermission as Qa}from"./permissions.mjs";const J=f.dirname(Re(import.meta.url));async function Zt(){const t=await m.stat(f.join(J,"..","hub","icon-180.png")).catch(()=>null);return`/_hub/icon-180.png?v=${t?Math.floor(t.mtimeMs).toString(36):"0"}`}u(Zt,"touchIcon");async function Va(t){if(!t.includes("<!--header "))return t;const e=Y.createContext({});return Y.runInContext(await m.readFile(f.join(J,"public","ui-page-header.js"),"utf8"),e),t.replace(/<!--header (\{.*?\})-->/g,(n,a)=>e.BoardUI.header(JSON.parse(a)))}u(Va,"pageHeaders");const d=f.resolve(process.argv[2]||process.cwd()),Xa=[f.join(d,"public"),f.join(J,"public"),f.join(J,"..","starter","public"),f.join(J,"..","shared")],v=f.join(d,"data"),C=f.join(v,"events.jsonl"),at=f.join(v,"replies"),qt=Number(process.env.PORT||4600),D=(process.env.PREFIX||"").replace(/\/+$/,""),te={".html":"text/html; charset=utf-8",".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".txt":"text/plain; charset=utf-8",".md":"text/markdown; charset=utf-8",".plist":"text/xml; charset=utf-8",".ipa":"application/octet-stream",".apk":"application/vnd.android.package-archive"},Ya=De(Ie.gzip),Za=/^(text\/|application\/json|image\/svg\+xml)/;async function ee(t,e,n,a){["GET","HEAD"].includes(t.req?.method)||H++;const r={...n};let i=Buffer.isBuffer(a)?a:Buffer.from(a??"");if(Za.test(r["content-type"]||"")&&(r.vary="accept-encoding",i.length>=1024&&/\bgzip\b/.test(t.req?.headers["accept-encoding"]||"")))try{i=await Ya(i),r["content-encoding"]="gzip"}catch{}r["content-length"]=i.length,t.writeHead(e,r),t.end(t.req?.method==="HEAD"?void 0:i)}u(ee,"send");const h=u((t,e,n,a={})=>ee(t,e,{"content-type":te[".json"],"cache-control":"no-store",...a},JSON.stringify(n)),"json"),nt=u(t=>`${Math.floor(t.mtimeMs).toString(36)}-${t.size.toString(36)}`,"versionOf"),ae=/\b(src|href)="([\w.][\w./-]*\.(?:js|css))"/g;async function qa(t,e){const n=e.endsWith("/")?e:f.posix.dirname(e),a=[...new Set([...t.matchAll(ae)].map(i=>i[2]))],r=new Map(await Promise.all(a.map(async i=>{const s=await fe(f.posix.join(n,i)),l=s?await m.stat(s).catch(()=>null):null;return[i,l?nt(l):null]})));return t.replace(ae,(i,s,l)=>r.get(l)?`${s}="${l}?v=${r.get(l)}"`:i)}u(qa,"versionAssets");const ne=u((t,e)=>String(t||"").split(",").map(n=>n.trim().replace(/^W\//,"")).includes(e.replace(/^W\//,"")),"sameTag"),b=u(async(t,e)=>{try{return JSON.parse(await m.readFile(t,"utf8"))}catch{return e}},"readJSON"),R=u(async t=>{try{return(await m.readFile(t,"utf8")).split(`
3
+ `).filter(Boolean).map(e=>JSON.parse(e))}catch{return[]}},"readLines"),M=u(t=>Ra(d,t),"threadFile"),pt=u(async()=>{const t=await Oa(d);return(await R(C)).filter(e=>!t.has(e.id))},"shownEvents");async function B(t,e=64*1024){if(!Ga(t.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});let n="";for await(const a of t)if(n+=a,n.length>e)throw Object.assign(new Error("too large"),{status:413});return n?JSON.parse(n):{}}u(B,"readBody");const $=u(async(t,e)=>(await it([[t,e]]))[0],"appendEvent");async function it(t){const e=new Date().toISOString(),n=t.map(([a,r])=>({id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,7),ts:e,kind:a,data:r}));return await m.mkdir(at,{recursive:!0}),await m.appendFile(C,n.map(a=>JSON.stringify(a)+`
4
+ `).join("")),n.map(a=>a.id)}u(it,"appendEvents");async function ie(t){const e=new Map(((await b(f.join(v,"owner.json"),{items:[]})).items||[]).map(s=>[s.id,s])),n=u(s=>e.has(String(s.id))&&Le(e.get(String(s.id)),s.choice),"again"),a=await it(t.map(s=>["owner-answer",n(s)?{...s,prototype_again:!0}:s]));await Aa(d,t),H++;const r=await x(),i=[...new Set(t.map(s=>Ge(r,String(s.id))).filter(Boolean))];return i.length&&await it(i.map(s=>["card-unblocked",{key:s}])),a}u(ie,"recordAnswers");async function tn(t){if(!Array.isArray(t)||!t.length)return"Send at least one answer.";const e=new Set(((await b(f.join(v,"owner.json"),{items:[]})).items||[]).map(a=>a.id)),n=t.filter(a=>!e.has(String(a?.id)));return n.length?`${n.map(a=>String(a?.id)).join(", ")} ${n.length===1?"is":"are"} no longer asked here. Reload to see what is, then send again.`:null}u(tn,"answersRefused");async function rt(t){for(const e of["html","json"]){const n=f.join(at,`${t.id}.${e}`);try{const[a,r]=await Promise.all([m.readFile(n,"utf8"),m.stat(n)]);return{...t,reply:{[e]:e==="json"?JSON.parse(a):a,at:r.mtime.toISOString()}}}catch{}}return{...t,reply:null}}u(rt,"withReply");async function en(t,e){const n=await mt(d);let a=await pt();return t&&(a=a.filter(r=>r.kind===t)),Promise.all(a.reverse().slice(0,e).map(async r=>{const i=await rt(r);return i.reply||!_(n,r.id)?i:{...i,working:_(n,r.id)}}))}u(en,"listEvents");const gt=new Map,an="number,title,url,state,isDraft,reviewDecision,mergeStateStatus,mergeable,statusCheckRollup,mergedAt,headRefName,headRefOid,updatedAt,latestReviews";function nn(t,e){return new Promise(n=>{Ht("gh",["pr","view",String(e),"--repo",t,"--json",an],{timeout:2e4},(a,r)=>{if(a)return n(null);try{n(JSON.parse(r))}catch{n(null)}})})}u(nn,"ghPR");function rn(t,e){return new Promise(n=>{Ht("gh",["api",`repos/${t}/pulls/${e}/comments?per_page=100`],{timeout:2e4},(a,r)=>{if(a)return n(0);try{const i=JSON.parse(r),s=new Set(i.map(l=>l.in_reply_to_id).filter(Boolean));n(i.filter(l=>!l.in_reply_to_id&&/codex/i.test(l.user?.login||"")&&!s.has(l.id)).length)}catch{n(0)}})})}u(rn,"ghCodexOpen");function sn(t){const e=t||[];let n=0,a=0;for(const r of e){const i=(r.conclusion||r.state||"").toUpperCase(),s=(r.status||"").toUpperCase();["FAILURE","ERROR","CANCELLED","TIMED_OUT","ACTION_REQUIRED","STARTUP_FAILURE"].includes(i)?n++:(s&&s!=="COMPLETED"||i==="PENDING"||i==="EXPECTED")&&a++}return{total:e.length,fail:n,pending:a,state:n?"fail":a?"pending":e.length?"pass":"none"}}u(sn,"checksOf");async function on(t,e){const n=`${t}#${e}`;let a=gt.get(n);return a||(a={at:0,value:null,loading:null},gt.set(n,a)),Date.now()-a.at>45e3&&!a.loading&&(a.loading=Promise.all([nn(t,e),rn(t,e)]).then(([r,i])=>{if(r){const{statusCheckRollup:s,latestReviews:l,...k}=r,p=Object.fromEntries((l||[]).map(O=>[O.author?.login,O.state])),y={...k,checks:sn(s),reviews:p,codexOpen:i};JSON.stringify(y)!==JSON.stringify(a.value)&&H++,a.value=y}a.at=Date.now(),a.loading=null})),!a.value&&a.loading&&await a.loading,a.value}u(on,"prState");const A=u(()=>ca(d),"config");async function cn(){const t=await m.readdir(at).catch(()=>[]),e=await Promise.all(t.map(a=>m.stat(f.join(at,a)).then(r=>r.mtimeMs,()=>0))),n=Math.max(0,...e);return n?new Date(n).toISOString():null}u(cn,"latestReply");async function re(t){let e=[];try{e=(await m.readdir(t)).filter(r=>r.endsWith(".json"))}catch{}const n=await Promise.all(e.map(r=>b(f.join(t,r),null))),a=Object.create(null);return e.forEach((r,i)=>{if(!n[i])return;const[s,l="main"]=f.basename(r,".json").split("@");(a[s]||=Object.create(null))[l]=n[i]}),a}u(re,"readAgents");async function yt(){const t=tt(d);return{cfg:await A(),github:t?.github,remote:t?.remote,branch:t?.branch,topicItems:Pe()}}u(yt,"outsideData");async function se(){return(await Promise.all((await q(d)).map(async t=>{const e=await b(t.file,null);return!e||!Array.isArray(e.items)?null:{slug:t.slug,wave:e,agents:await re(f.join(t.dir,"agents"))}}))).filter(Boolean)}u(se,"liveWaves");async function ln(t){const e=await se();for(const n of t){const a=e.find(r=>r.slug===n.wave);a?a.wave.items.push(n.item):e.push({slug:n.wave,wave:{items:[n.item]},agents:{}})}return e}u(ln,"withArchived");async function oe({cfg:t,github:e,remote:n,branch:a},r){const i=t.items==="waves";r??=await se();const s=Object.create(null);return await Promise.all(r.flatMap(({slug:l,wave:k})=>k.items.map(async p=>{s[`${l}/${p.id}`]=await R(M(`${l}/${p.id}`))}))),{github:e,remote:n,branch:a,reviewers:t.reviewers||[],fromWaves:i,autoMerge:T(t).auto_merge,skipDraft:T(t).skip_draft,autoPull:T(t).auto_pull,autoBuild:T(t).auto_build,tickets:T(t).tickets,waves:r,threads:s,working:await mt(d),autoReview:T(t).auto_review,incoming:await b(f.join(v,"incoming.json"),null),reviewRecords:await b(f.join(v,"reviews.json"),{})}}u(oe,"itemSnapshot");async function ce(t,e=[]){const n=await oe(t,e.length?await ln(e):void 0),{waves:a,threads:r,working:i}=n,s=Object.create(null),l=new Set(e.map(y=>y.key));await Promise.all(a.flatMap(({slug:y,wave:O})=>{const N=n.github||O.repo;return O.items.flatMap(c=>Ze(c,N).map(async o=>{s[Mt(o)]=l.has(`${y}/${c.id}`)?gt.get(Mt(o))?.value??null:await on(o.repo,o.number)}))}));const k=new Set(["ask","owner-answer","grill-more","item-message","wave-triage","overnight-triage"]),p=await Promise.all((await pt()).map(y=>k.has(y.kind)||_(i,y.id)?rt(y):{...y,reply:null}));return await Promise.all(p.filter(y=>y.kind==="ask").map(async y=>{const O=await R(M(`ask:${y.id}`));O.length&&(r[`ask:${y.id}`]=O)})),{...n,try:await b(f.join(v,"try.json"),null),main:await b(f.join(v,"main.json"),null),prs:s,events:p,askLinks:await wt(d),askProgress:await ka(d),checked:await b(f.join(v,"checked.json"),null),watcher:await b(f.join(v,"watcher.json"),null),replyAt:await cn(),owner:(y=>({...y,items:[...y.items||[],...e.flatMap(O=>O.questions||[])]}))(await b(f.join(v,"owner.json"),{items:[]})),topicItems:t.topicItems}}u(ce,"snapshot");async function un(t){if(!t.length)return[];const e=new Map(dt(await ce(await yt(),t)).items.map(n=>[n.key,n]));return t.map(n=>{const a=e.get(n.key);return Ut({...a,sortAt:n.closed_at,worktree:a.worktree&&!Lt(a.worktree)?null:a.worktree})})}u(un,"archivedCards");const st=D.slice(1)||f.basename(d);let H=0,L=null;async function x(){const t=await yt(),e=JSON.stringify([H,t]);if(L?.key!==e){const n=ce(t).then(dt);L={key:e,built:n},n.catch(()=>{L?.built===n&&(L=null)})}return L.built}u(x,"sharedBoard");let kt=!1;async function St(){if(!kt){kt=!0;try{const t=f.join(v,"builder-stalled.json"),e=await b(t,{}),n=Object.create(null);let a;for(const{key:r,lane:i,builder:s}of(await x()).items)i==="done"||!s?.stalled||(n[r]=s.silentSince,_(e,r)!==s.silentSince&&(a||=await R(C),a.some(l=>l.kind==="builder-stalled"&&l.data?.key===r&&l.data?.silentSince===s.silentSince)||await $("builder-stalled",{key:r,silentSince:s.silentSince})));JSON.stringify(n)!==JSON.stringify(e)&&(await m.mkdir(v,{recursive:!0}),await m.writeFile(t+".tmp",JSON.stringify(n)+`
5
+ `),await m.rename(t+".tmp",t))}catch(t){console.error("Builder check failed:",t.message)}finally{kt=!1}}}u(St,"checkStalled");async function dn(t){const e=await ba(d);if(!e?.hello?.id||e.done_at)return null;const n=t?.find(r=>r.link==="github"),a={events:e.ack?.id===e.hello.id,...n||!t?{github:!!n?.ok}:{}};return a.events&&a.github!==!1?(await ya(d),null):a}u(dn,"setupOf");const fn=Ua.map(([t,e])=>({value:t,label:e,ms:t===Vt?null:Xt(new Date(0),t)-new Date(0)}));async function W(){const t=new Date().toISOString(),{title:e,reviewers:n}=await A(),a=tt(d),r=a?Ha(a):"deaf",i=Be(await x(),{listen:r,prompts:a?Fa(a.route).map(Ja):[]});i.items=i.items.map(l=>l.worktree&&!Lt(l.worktree)?{...l,worktree:null}:l);const s=a?Ca(a,{listen:r}):null;return{generatedAt:t,title:e,reviewers:(n||[]).map(l=>l?.name).filter(l=>typeof l=="string"),...i,links:s,setup:await dn(s),holdChoices:fn,arch:a?Ae(await $e(d),i.items,i.owner,{repo:xe(Yt(a.route)),waits:za()}):null}}u(W,"board");async function hn(t){["GET","HEAD"].includes(t.method)||H++;const e={...await W(),onHost:await Ee(t),doneArchived:await sa(d)};e.owner=e.owner.filter(s=>s.state!=="resolved"),e.items=e.items.map(s=>s.lane==="done"?Ut(s):s);const n=new Set(e.items.map(s=>s.key));e.ownerTopics=e.ownerTopics.map(({title:s,summary:l,askedAt:k,...p})=>n.has(p.card)?p:{...p,title:s,summary:l,...k?{askedAt:k}:{}});const{generatedAt:a,...r}=e,i={...e,version:Ct.createHash("sha1").update(JSON.stringify(r)).digest("base64url").slice(0,20)};G.delete(i.version),G.set(i.version,i);for(const s of[...G.keys()].slice(0,-wn))G.delete(s);return i}u(hn,"pageBoard");const wn=8,G=new Map,le=Y.createContext({});Y.runInContext(await m.readFile(new URL("./public/ui-patch.js",import.meta.url),"utf8"),le);const{boardPatch:mn}=le.BoardUI;async function F(t,e){const n=e&&G.get(e),a=await hn(t);return n?mn(n,a):a}u(F,"answerBoard");let ue=Promise.resolve();function pn(){const t=ue.then(async()=>{const e=tt(d);if(!e)throw Object.assign(new E("This board isn\u2019t in the hub\u2019s manifest."),{status:404});const{arch:n}=await W();if(n.last?.open)return{key:n.last.key,ref:n.last.ref,existing:!0};const{key:a,data:r}=await Ne(d,e),i=await $("arch-review",r);return{key:a,id:i,existing:!1}});return ue=t.catch(()=>{}),t}u(pn,"runReview");async function gn(t){const[e,n]=t.split("/"),a=(await q(d)).find(l=>l.slug===e),r=a?await b(a.file,null):null,i=!r?.items?.some(l=>l.id===n)&&(await Gt(d)).find(l=>l.key===t),s=Array.isArray(r?.items)&&r.items.find(l=>l.id===n)||i?.item||null;return{item:s,questions:i?.questions||[],agents:s&&a?_(await re(f.join(a.dir,"agents")),n)||{}:{}}}u(gn,"itemOf");async function yn(t){const e=_(await wt(d),t),{tickets:n}=T(await A()),a=[];for(const r of await q(d)){const i=await b(r.file,null);for(const s of Array.isArray(i?.items)?i.items:[]){const l=`${r.slug}/${s.id}`;s.from_ask===t?a.unshift({key:l,ref:ft(i,s,n),became:!0}):l===e&&a.push({key:l,ref:ft(i,s,n),became:!1})}}return a}u(yn,"askLinksOf");async function kn(t){const{item:e,agents:n,questions:a}=t.startsWith("ask:")?{item:null,agents:{},questions:[]}:await gn(t),r=await b(f.join(v,"owner.json"),{items:[]}),i={...r,items:[...r.items||[],...a]},s=new Set(i.items.filter(o=>o.topic===`item:${t}`).map(o=>o.id)),l=t.startsWith("ask:"),k=l?t.slice(4):e?.from_ask||null,p=l?[]:Object.entries(await wt(d)).filter(([,o])=>o===t).map(([o])=>o),y=[k,...p].filter(Boolean),O=new Set([t,...y.map(o=>`ask:${o}`)]),N=(await pt()).filter(o=>O.has(o.data?.key)&&Fe.includes(o.kind)||o.kind==="owner-answer"&&s.has(o.data?.id)||o.kind==="ask"&&y.includes(o.id)||o.kind==="ask-edited"),c=(await Promise.all([...O].map(o=>R(M(o))))).flat();return Ke(t,{lines:c,events:await Promise.all(N.map(rt)),owner:i,working:await mt(d),fromAsk:k,links:l?await yn(k):[],agents:n,moved:p})}u(kn,"thread");const de=u(t=>String(t).replace(/[&<>"']/g,e=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[e]),"esc");async function fe(t){for(const e of Xa){let n=f.resolve(e,"."+t);if(n!==e&&!n.startsWith(e+f.sep))return null;if((await m.stat(n).catch(()=>null))?.isDirectory()&&(n=f.join(n,"index.html")),await m.stat(n).then(r=>r.isFile(),()=>!1))return n}return null}u(fe,"staticFile");const he=f.join(J,"public","prototype.html");async function Sn(t){const e=/^\/prototype-([a-z0-9-]+)\.html$/.exec(t)?.[1];return e&&(await pa(d)).find(n=>n.topic===e)||null}u(Sn,"recordedPrototype");const we=u(t=>JSON.stringify(t).replace(/</g,"\\u003c").replace(/\u2028/g,"\\u2028").replace(/\u2029/g,"\\u2029"),"scriptJSON");async function vn(t){const e=await Promise.all(t.takes.map(async i=>{const s=`prototypes/${t.topic}/${i.key}.js`,l=await m.stat(ga(d,t.topic,i.key)).catch(()=>null);return{key:i.key,label:i.label,src:l?.isFile()?`${s}?v=${nt(l)}`:null,file:s}})),n=`${t.title} \xB7 prototype`,a={lead:"back",backHref:t.card?`./#item=${encodeURIComponent(t.card)}`:"./",title:n,controls:!1},r={prototypeTitle:de(n),prototypeHeader:`<!--header ${we(a).replace(/>/g,"\\u003e")}-->`,prototypeData:we({topic:t.topic,title:t.title,takes:e})};return(await m.readFile(he,"utf8")).replace(/\{\{(prototypeTitle|prototypeHeader|prototypeData)\}\}/g,(i,s)=>r[s])}u(vn,"prototypePage");async function bn(t,e){try{const n=decodeURIComponent(String(t.headers["x-filename"]||""));return h(e,200,await _a(d,t,{name:n,type:t.headers["content-type"],length:Number(t.headers["content-length"])||0}))}catch(n){if(!(n instanceof E)&&!(n instanceof URIError))throw n;return t.resume(),h(e,n.status||400,{error:n.message})}}u(bn,"upload");const On=/^(text\/html|image\/svg\+xml|application\/(xhtml\+)?xml|text\/xml)$/;async function jn(t,e,n){const a=await Ea(d,n);if(!a){e.writeHead(404).end();return}if(e.writeHead(200,{"content-type":a.type.startsWith("text/")?`${a.type}; charset=utf-8`:a.type,"content-length":a.size,"content-disposition":`inline; filename*=UTF-8''${encodeURIComponent(a.name)}`,"cache-control":"private, max-age=86400","x-content-type-options":"nosniff",...On.test(a.type)?{"content-security-policy":"sandbox"}:{}}),t.method==="HEAD"){e.end();return}ta(a.path).pipe(e)}u(jn,"sendUpload");const vt=new Set;function Pn(t,e){e.writeHead(200,{"content-type":"text/event-stream; charset=utf-8","cache-control":"no-store",connection:"keep-alive"}),e.write(`retry: 3000
6
6
 
7
- `),at.add(e),t.on("close",()=>at.delete(e))}c(ga,"stream");const Tt=c(t=>{for(const e of at)e.write(t)},"broadcast");setInterval(()=>Tt(`: ping
7
+ `),vt.add(e),t.on("close",()=>vt.delete(e))}u(Pn,"stream");const me=u(t=>{for(const e of vt)e.write(t)},"broadcast");setInterval(()=>me(`: ping
8
8
 
9
- `),25e3).unref();const ya=100,ka=500;let V=null,$t=0;function At(){const t=Date.now();V||($t=t),clearTimeout(V),V=setTimeout(()=>{V=null,Tt(`event: change
9
+ `),25e3).unref();const En=100,Tn=500;let ot=null,pe=0;function bt(){H++;const t=Date.now();ot||(pe=t),clearTimeout(ot),ot=setTimeout(()=>{ot=null,me(`event: change
10
10
  data: ${JSON.stringify({at:new Date().toISOString()})}
11
11
 
12
- `),et(),ct(),rt()},Math.max(0,Math.min(ya,$t+ka-t)))}c(At,"changed");const xt=c(t=>{try{return ie(t).isDirectory()}catch{return!1}},"isFolder");function Sa(t){const e=new Map;let n=!1;const a=c(l=>{for(const[m,h]of e)(m===l||m.startsWith(l+p.sep))&&(h.close(),e.delete(m))},"unwatch"),i=c(l=>{if(e.has(l))return;const m=re(l,(h,j)=>{if(String(j||"").endsWith(".tmp")||At(),l===t&&!xt(t))return s();if(!j)return;const w=p.join(l,String(j));if(!xt(w))a(w);else try{i(w)}catch{a(w)}});e.set(l,m),m.on("error",()=>l===t?s():a(l));for(const h of ne(l,{withFileTypes:!0}))if(h.isDirectory())try{i(p.join(l,h.name))}catch{a(p.join(l,h.name))}},"watchFolder");let r=null;const s=c(()=>{r||(a(t),n=!0,r=setTimeout(()=>{r=null,o()},2e3).unref())},"retry"),o=c(()=>{try{i(t)}catch{s();return}n&&At()},"start");o()}c(Sa,"watchTree");async function ba(){const t=(await T()).items==="waves"?[Re()]:(await J(d)).map(n=>n.dir),e=[...new Set([b,...t].map(n=>p.resolve(n)))];for(const n of e.filter(a=>!e.some(i=>i!==a&&a.startsWith(i+p.sep))))Sa(n)}c(ba,"watchBoard");const va=Jt.createServer(async(t,e)=>{try{if(t.method!=="GET"&&t.method!=="HEAD"&&Le(t.headers)){e.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),e.end("Requests must come from the app\u2019s own pages.");return}const n=new URL(t.url,"http://site");let a=n.pathname;if(R&&(a===R||a.startsWith(R+"/"))&&(a=a.slice(R.length)||"/"),a==="/_claude/health")return g(e,200,{ok:!0});if(a==="/api/board")return g(e,200,{...await $(),onHost:Ft(t)});if(a==="/api/links")return g(e,200,{links:(await $()).links});if(a==="/api/stream"&&t.method==="GET")return ga(t,e);if(a==="/_claude/needs")return g(e,200,Qt(await $()));if(a==="/api/working"){const u=A(await Q({github:!1})),f=new Map(u.items.filter(S=>S.kind==="work").map(S=>[S.key,S.ref]));return g(e,200,{working:u.workingOn.map(S=>({id:S.id,kind:S.kind,card:f.get(S.key)||null,since:S.since}))})}if(a==="/api/thread")return g(e,200,await da(String(n.searchParams.get("key")||"")));if(a==="/api/notes"&&n.searchParams.has("wave"))try{return g(e,200,await ve(d,String(n.searchParams.get("wave")),x(await T()).tickets))}catch(u){if(u instanceof P)return g(e,404,{error:u.message});throw u}if(a==="/api/notes"||a==="/api/plan"){const u=String(n.searchParams.get("item")||"");try{const{wave:f,item:S,markdown:k}=await je(d,u);return g(e,200,{key:u,ref:X(f,S,x(await T()).tickets),title:S.title,markdown:k})}catch(f){if(f instanceof P)return g(e,404,{error:f.message});throw f}}if(a==="/_claude/events"&&t.method==="POST"){const u=await _(t,524288),f=String(u.kind||"message").slice(0,64);if(delete u.kind,f==="card-hold"&&u.hold&&u.for!==void 0){const k=Ue(new Date,u.for);if(!k)return g(e,400,{error:"That hold ends in the past or more than 30 days on. Pick another time."});delete u.for,k!==Be&&(u.until=k.toISOString())}if(f==="card-build"){const k=String(u.key||""),D=await fe(d,k,()=>lt(d,k.split("/")[1]));return g(e,200,{id:D&&!D.queued?await O(f,D):null,board:await $()})}if(f==="owner-answer")return g(e,200,{id:(await kt([u]))[0],board:await $()});if(f==="owner-answers"){const k=await qe(u.answers);return k?g(e,400,{error:k}):g(e,200,{ids:await kt(u.answers),board:await $()})}const S=await O(f,u);if(f==="card-hold"||f==="card-done")try{await me(d,f,u)}catch(k){console.error(`Recording ${f} failed:`,k.message)}return g(e,200,{id:S,board:await $()})}if(a==="/api/rules"&&t.method==="GET")return g(e,200,{board:await ke(d),hub:Ne()});if(a==="/api/rules"&&t.method==="POST"){const u=await _(t);let f;try{f=await Ee(d,u.rules,u.base??[])}catch(k){if(k instanceof P)return g(e,400,{error:k.message});throw k}const S=Ie(f.changes)?await O("rules",{scope:"board",...f.changes}):null;return g(e,200,{rules:f.rules,id:S})}if(a==="/api/settings"&&t.method==="GET")return g(e,200,{settings:await Se(d)});if(a==="/api/try/main/log"&&t.method==="GET"){let u;try{u=await y.open(p.join(d,"data","try","main.log"),"r");const{size:f}=await u.stat(),S=Math.max(0,f-64*1024),k=Buffer.alloc(f-S),{bytesRead:D}=await u.read(k,0,k.length,S),C=k.subarray(0,D).toString("utf8").split(/\r?\n/);return S&&C.shift(),C.at(-1)===""&&C.pop(),g(e,200,{lines:C.slice(-40)})}catch(f){if(f.code==="ENOENT")return g(e,200,{lines:[]});throw f}finally{await u?.close()}}if(a==="/api/try"&&t.method==="POST"){const u=await _(t);if(!u||typeof u!="object"||Array.isArray(u)||Object.keys(u).some(f=>f!=="key")||typeof u.key!="string")return g(e,400,{error:"Send only the card key."});try{const f=await fetch(`http://127.0.0.1:${process.env.KICKFLIP_HUB_PORT||4500}/_hub/build`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({route:R.slice(1)||p.basename(d),key:u.key}),signal:AbortSignal.timeout(1e4)});return g(e,f.status,await f.json())}catch(f){return g(e,502,{error:`Could not start the build: ${f.message}`})}}if(a==="/api/settings"&&t.method==="POST"){const u=await _(t);let f;try{f=await Te(d,u)}catch(k){if(k instanceof P)return g(e,400,{error:k.message});throw k}const S=f.changed.length?await O("settings",Object.fromEntries(f.changed.map(k=>[k,f.settings[k]]))):null;return f.changed.includes("auto_merge")&&f.settings.auto_merge&&await O("check-github",{reason:"auto-merge"}),g(e,200,{settings:f.settings,id:S})}if(a==="/api/arch-review"&&t.method==="POST"){await _(t);try{return g(e,200,await ca())}catch(u){if(u instanceof P)return g(e,u.status||400,{error:u.message});throw u}}if(a==="/api/hide"&&t.method==="POST"){const u=await _(t);try{await Ae(d,String(u.id||""),u.hidden!==!1)}catch(f){if(f instanceof P)return g(e,400,{error:f.message});throw f}return g(e,200,{ok:!0})}if(a==="/_claude/events"&&t.method==="GET")return g(e,200,await ta(n.searchParams.get("kind"),Math.min(500,Number(n.searchParams.get("limit")||100))));if(a==="/api/upload"&&t.method==="POST")return await ma(t,e);if(t.method!=="GET"&&t.method!=="HEAD"){e.writeHead(405).end();return}if(a.startsWith("/uploads/"))return await ha(t,e,decodeURIComponent(a.slice(9)));if(a==="/plan.html"){e.writeHead(302,{location:`notes.html${n.search}`,"cache-control":"no-store"}).end();return}if(a==="/index.html"){e.writeHead(302,{location:`./${n.search}`,"cache-control":"no-store"}).end();return}const i=Je.has(a.slice(1)),r=i?null:await fa(a),s=i?null:r?Pt:a==="/prototype.html"?null:await Ot(decodeURIComponent(a));if(!i&&!s){e.writeHead(404).end();return}const o=i?".html":p.extname(s).toLowerCase(),l=i?null:await y.stat(s);let m=i?Buffer.from(Me({touchIcon:await mt()})):r?Buffer.from(await pa(r)):await y.readFile(s);!i&&o===".html"&&(m=(await Qe(m.toString("utf8"))).replaceAll("{{title}}",jt((await T()).title)).replaceAll("{{touchIcon}}",await mt())),!i&&o===".html"&&(m=Buffer.from(await Ye(m,a)));const h=o===".html"?`W/"${Wt.createHash("sha1").update(m).digest("base64url").slice(0,20)}"`:`W/"${L(l)}"`,j=o!==".html"&&n.searchParams.get("v")===L(l),w={"content-type":E(ht,o)||"application/octet-stream","cache-control":j?"public, max-age=31536000, immutable":"no-cache",etag:h};if(Ze(t.headers["if-none-match"],h)){e.writeHead(304,{etag:h,"cache-control":w["cache-control"],vary:"accept-encoding"}).end();return}return await gt(e,200,w,m)}catch(n){e.headersSent||(n.status===403?e.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}).end(n.message):e.writeHead(n.status||(n.code==="ENOENT"?404:400)).end())}});let nt=!1;async function Rt(){if(!nt){nt=!0;try{const t=Date.now(),e=A({...await z(),events:await W(),now:t}).items.filter(a=>a.heldUntil&&a.lane!=="done"&&Date.parse(a.heldUntil)<=t),n=e.length?await I(F):[];for(const{key:a,heldUntil:i}of e)await ue(d,a,i,{announce:!n.some(r=>r.kind==="card-unblocked"&&r.data?.key===a&&r.data?.until===i)})}catch(t){console.error("Pick-up check failed:",t.message)}finally{nt=!1}}}c(Rt,"checkPickUps");let B=null,it=!1;function rt(){return B?(it=!0,B):(B=(async()=>{do{it=!1;try{if(pt().state==="paused")continue;for(const t of await ce(d)){const e=await de(d,t,()=>lt(d,t.split("/")[1]));e&&!e.queued&&await O("card-build",e)}}catch(t){console.error("Queued build check failed:",t.message)}}while(it)})().finally(()=>{B=null}),B)}c(rt,"checkQueued");let st=!1;async function It(){if(!st){st=!0;try{const t=x(await T()),e=await I(F),n=await ft(d),{items:a}=A({...await z(),events:e.filter(l=>!n.has(l.id)),owner:await v(p.join(b,"owner.json"),{items:[]})}),{idle:i,pullable:r,plannable:s}=Xt(a),o=i&&pt().state!=="paused";if(await ja(t,e,a,o))return;await Nt(Oa,t,e,r,o),await Nt(Pa,t,e,r.length?[]:s,o)}catch(t){console.error("Idle check failed:",t.message)}finally{st=!1}}}c(It,"checkIdle");async function ja(t,e,n,a){const i=p.join(b,"triaged.json"),r=await v(i,{}),s=Zt(n),o=c(async()=>{await y.mkdir(b,{recursive:!0}),await y.writeFile(i+".tmp",JSON.stringify({todo:s})+`
13
- `),await y.rename(i+".tmp",i)},"save"),l=e.filter(w=>w.kind==="todo-triage").at(-1);if(l&&!(await G(l)).reply)return r.todo!==s&&await o(),!0;const m=qt(new Date);if(!t.auto_triage||!a||!s||s===r.todo||l?.data?.date===m)return!1;const h=`todo-triage-${m}`;return((await v(p.join(b,"owner.json"),{topics:[]})).topics||[]).some(w=>w.id===h)||await se(d,{id:h,title:`Todo triage ${m}`}),await O("todo-triage",{date:m,topic:h}),await o(),!0}c(ja,"checkTriage");const Oa={kind:"card-pull",file:"pulled.json",line:"Pulled in: the board was idle."},Pa={kind:"card-plan",file:"planned.json",line:"Asked for a grill or prototype: nothing in Todo was ready to build."};async function Nt({kind:t,file:e,line:n},a,i,r,s){const o=p.join(b,e),l=await v(o,{}),m=c(async u=>{await y.mkdir(b,{recursive:!0}),await y.writeFile(o+".tmp",JSON.stringify({key:u,at:new Date().toISOString()})+`
14
- `),await y.rename(o+".tmp",o)},"save");if(!a.auto_pull){l.key&&await m(null);return}const h=i.filter(u=>u.kind===t).at(-1),j=h&&String(h.ts)>String(l.at||"")?h.data?.key:l.key;if(j&&r.includes(j))return;if(!s||!r.length){j&&await m(null);return}const w=r[0];await O(t,{key:w}),await y.mkdir(p.dirname(H(w)),{recursive:!0}),await y.appendFile(H(w),JSON.stringify({ts:new Date().toISOString(),who:"claude",text:n})+`
15
- `),await m(w)}c(Nt,"checkPull");let U=null,ot=!1;function ct(){return U?(ot=!0,U):(U=(async()=>{do{ot=!1;try{await Ea()}catch(t){console.error("Action push check failed:",t.message)}}while(ot)})().finally(()=>{U=null}),U)}c(ct,"checkActions");async function Ea(){const t=p.join(b,"action-pushes.json"),e=await v(t,null),n=await Q(),a=Vt(A(n)),i=Object.fromEntries(a.map(r=>[r.tag,{kind:r.kind,ids:r.questions.map(s=>s.id)}]));if(e){for(const s of a){const o=E(e,s.tag),l=s.questions.filter(h=>!o?.ids?.includes(h.id));if(o?.kind===s.kind&&!l.length)continue;const m=l.length?`questions.html#${encodeURIComponent(l[0].id)}`:s.url;try{await pe(d,{kind:"action",title:s.title,body:l[0]?.question||s.body,url:`/${n.route}/${m}`,tag:s.tag,now:s.now})}catch(h){if(h instanceof P)console.error(`Push ${s.tag} not sent:`,h.message.split(`
16
- `).at(-2)||h.message);else throw h}}const r=Object.keys(e).filter(s=>!E(i,s));r.length&&await te(r,n.route)}JSON.stringify(i)!==JSON.stringify(e)&&(await y.mkdir(b,{recursive:!0}),await y.writeFile(t+".tmp",JSON.stringify(i)+`
17
- `),await y.rename(t+".tmp",t))}c(Ea,"pushActions");try{for(const t of await le(d))console.log(t)}catch(t){console.error("follow-up migration:",t.message)}await ba(),setInterval(et,6e4).unref(),setInterval(Rt,6e4).unref(),setInterval(It,6e4).unref(),setInterval(ct,6e4).unref(),setInterval(rt,6e4).unref(),await et(),await Rt(),await It(),ct(),rt(),va.listen(wt,"127.0.0.1",()=>console.log(`site on http://127.0.0.1:${wt}/ (prefix ${R||"none"})`));
12
+ `),be(),St(),_t(),lt(),It()},Math.max(0,Math.min(En,pe+Tn-t)))}u(bt,"changed");const An=new Set(["node_modules","dd","DerivedData"]),ct=u(t=>String(t).endsWith(".tmp")||String(t).split(/[\\/]/).some(e=>e.startsWith(".")||An.has(e)),"ignored"),Ot=u(t=>{try{return aa(t).isDirectory()}catch{return!1}},"isFolder");function $n(t){let e=u(()=>{},"stop"),n=!1,a=null;const r=u(()=>{a||(e(),n=!0,a=setTimeout(()=>{a=null,i()},2e3).unref())},"retry"),i=u(()=>{try{e=(process.platform==="linux"?Nn:xn)(t,r)}catch{r();return}n&&bt()},"start");i()}u($n,"watchTree");function xn(t,e){const n=Wt(t,{recursive:!0},(a,r)=>{(!r||!ct(r))&&bt(),Ot(t)||e()});return n.on("error",e),()=>n.close()}u(xn,"watchRecursive");function Nn(t,e){const n=new Map,a=u(i=>{for(const[s,l]of n)(s===i||s.startsWith(i+f.sep))&&(l.close(),n.delete(s))},"unwatch"),r=u(i=>{if(n.has(i))return;const s=Wt(i,(l,k)=>{if((!k||!ct(k))&&bt(),i===t&&!Ot(t))return e();if(!k||ct(k))return;const p=f.join(i,String(k));if(!Ot(p))a(p);else try{r(p)}catch{a(p)}});n.set(i,s),s.on("error",()=>i===t?e():a(i));for(const l of ea(i,{withFileTypes:!0}))if(!(!l.isDirectory()||ct(l.name)))try{r(f.join(i,l.name))}catch{a(f.join(i,l.name))}},"watchFolder");try{r(t)}catch(i){throw a(t),i}return()=>a(t)}u(Nn,"watchEachFolder");async function _n(){const t=(await A()).items==="waves"?[Ia()]:(await q(d)).map(n=>n.dir),e=[...new Set([v,...t].map(n=>f.resolve(n)))];for(const n of e.filter(a=>!e.some(r=>r!==a&&a.startsWith(r+f.sep))))$n(n)}u(_n,"watchBoard");const Rn=_e.createServer(async(t,e)=>{try{if(t.method!=="GET"&&t.method!=="HEAD"&&H++,t.method!=="GET"&&t.method!=="HEAD"&&Wa(t.headers)){e.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),e.end("Requests must come from the app\u2019s own pages.");return}const n=new URL(t.url,"http://site");let a=n.pathname;if(D&&(a===D||a.startsWith(D+"/"))&&(a=a.slice(D.length)||"/"),a==="/_claude/health")return h(e,200,{ok:!0});if(a==="/api/board"){const c=await F(t,String(n.searchParams.get("since")||"")),o=`W/"${c.version}"`;if(ne(t.headers["if-none-match"],o)){e.writeHead(304,{etag:o,"cache-control":"no-store",vary:"accept-encoding"}).end();return}return h(e,200,c,{etag:o})}if(a==="/api/questions"){const c=await x(),o=new Set(c.ownerTopics.find(w=>w.id===String(n.searchParams.get("topic")??""))?.questions||[]);return h(e,200,{questions:c.owner.filter(w=>o.has(w.id)&&w.state==="resolved")})}if(a==="/api/done"){const c=String(n.searchParams.get("before")||""),o=(await Gt(d)).filter(w=>!c||oa(w)<c);return h(e,200,{cards:await un(o.slice(0,Jt)),left:Math.max(0,o.length-Jt)})}if(a==="/api/links")return h(e,200,{links:(await W()).links});if(a==="/api/stream"&&t.method==="GET")return Pn(t,e);if(a==="/_claude/needs")return h(e,200,Ue(await W()));if(a==="/api/working"){const c=await x(),o=new Map(c.items.filter(w=>w.kind==="work").map(w=>[w.key,w.ref]));return h(e,200,{working:c.workingOn.map(w=>({id:w.id,kind:w.kind,card:o.get(w.key)||null,since:w.since}))})}if(a==="/api/thread")return h(e,200,await kn(String(n.searchParams.get("key")||"")));if(a==="/api/notes"&&n.searchParams.has("wave"))try{return h(e,200,await ja(d,String(n.searchParams.get("wave")),T(await A()).tickets))}catch(c){if(c instanceof E)return h(e,404,{error:c.message});throw c}if(a==="/api/notes"||a==="/api/plan"){const c=String(n.searchParams.get("item")||"");try{const{wave:o,item:w,markdown:S}=await Pa(d,c);return h(e,200,{key:c,ref:ft(o,w,T(await A()).tickets),title:w.title,markdown:S})}catch(o){if(o instanceof E)return h(e,404,{error:o.message});throw o}}if(a==="/_claude/events"&&t.method==="POST"){const c=await B(t,524288),o=String(c.kind||"message").slice(0,64);delete c.kind;const w=String(c.since||"");if(delete c.since,o==="card-hold"&&c.hold&&c.for!==void 0){const g=Xt(new Date,c.for);if(!g)return h(e,400,{error:"That hold ends in the past or more than 30 days on. Pick another time."});delete c.for,g!==Vt&&(c.until=g.toISOString())}if(o==="card-build"){const g=String(c.key||""),P=await zt(d,g,j=>ut(d,g.split("/")[1],j));return h(e,200,{id:await Et(P),board:await F(t,w)})}if(o==="lineup-move"){const g=String(c.lineup||""),P=Je(await x(),g,String(c.key||""),c.by===-1?-1:c.by===1?1:0);if(!P)return h(e,400,{error:`That card can't move that way in the wave "${g}".`});try{await la(d,g,{phases:P.map(j=>j.join(","))})}catch(j){if(j instanceof E)return h(e,400,{error:j.message});throw j}return h(e,200,{id:null,board:await F(t,w)})}if(o==="owner-answer")return h(e,200,{id:(await ie([c]))[0],board:await F(t,w)});if(o==="owner-answers"){const g=await tn(c.answers);return g?h(e,400,{error:g}):h(e,200,{ids:await ie(c.answers),board:await F(t,w)})}if(o==="card-hold"||o==="card-done"){const g=String(c.key||"");let P;try{P=await ma(d,o,c)}catch(j){return h(e,j instanceof E?400:500,{error:`Couldn\u2019t write ${g} to its wave file: ${j.message}`})}if(!P)return h(e,400,{error:`${g} is already merged or dropped, so its wave file stays as it is.`})}if(o==="reviewers"){const g=u(I=>I===void 0?[]:Array.isArray(I)&&I.every(X=>typeof X=="string"&&X.trim())?I.map(X=>X.trim()):null,"names"),P=g(c.add),j=g(c.remove);if(!P||!j||!P.length&&!j.length)return h(e,400,{error:"Send the names to add or remove, each as a list."});if(j.length)try{await Ta(d,j)}catch(I){if(I instanceof E)return h(e,400,{error:I.message});throw I}const Oe=await it([...j.length?[["reviewers-removed",{remove:j}]]:[],...P.length?[["reviewers",{add:P}]]:[]]);return h(e,200,{id:Oe.at(-1),board:await F(t,w)})}const S=await $(o,c);return o==="prototype-request"&&c.question&&await $a(d,String(c.question)),o==="ask-withdrawn"&&await Qt(d,String(c.id||""),!0),h(e,200,{id:S,board:await F(t,w)})}if(a==="/api/rules"&&t.method==="GET")return h(e,200,{board:await Sa(d),hub:Ba()});if(a==="/api/rules"&&t.method==="POST"){const c=await B(t);let o;try{o=await xa(d,c.rules,c.base??[])}catch(S){if(S instanceof E)return h(e,400,{error:S.message});throw S}const w=Da(o.changes)?await $("rules",{scope:"board",...o.changes}):null;return h(e,200,{rules:o.rules,id:w})}if(a==="/api/settings"&&t.method==="GET")return h(e,200,{settings:await va(d)});if(a==="/api/permissions"){const c=tt(d);if(!c)return h(e,404,{error:"This board isn\u2019t in the hub\u2019s manifest."});const o=Yt(c.route).repo,w=t.method==="POST"?await B(t):null,S=await Te(t);if(w&&!S)return h(e,403,{error:"Only the signed-in owner can change these."});try{return w&&await Qa(o,d,w.id,w.on),h(e,200,await Ka(o,d,S))}catch(g){if(g instanceof E)return h(e,400,{error:g.message});throw g}}if(a==="/api/try/main/log"&&t.method==="GET"){let c;try{c=await m.open(f.join(d,"data","try","main.log"),"r");const{size:o}=await c.stat(),w=Math.max(0,o-64*1024),S=Buffer.alloc(o-w),{bytesRead:g}=await c.read(S,0,S.length,w),P=S.subarray(0,g).toString("utf8").split(/\r?\n/);return w&&P.shift(),P.at(-1)===""&&P.pop(),h(e,200,{lines:P.slice(-40)})}catch(o){if(o.code==="ENOENT")return h(e,200,{lines:[]});throw o}finally{await c?.close()}}if(a==="/api/log"&&t.method==="GET"){const c=n.searchParams.get("file")||"";if(!/^[\w.-]+\.log$/.test(c)||c.startsWith("."))return h(e,400,{error:"file takes a log file\u2019s name."});let o;try{o=await m.open(f.join(Bt.homedir(),".claude","kickflip","logs",c),"r");const{size:w}=await o.stat(),S=Math.max(0,w-256*1024),g=Buffer.alloc(w-S),{bytesRead:P}=await o.read(g,0,g.length,S),j=g.subarray(0,P).toString("utf8");return h(e,200,{text:S?j.slice(j.indexOf(`
13
+ `)+1):j,truncated:S>0,size:w})}catch(w){if(w.code==="ENOENT")return h(e,404,{error:"No such log."});throw w}finally{await o?.close()}}if(a==="/api/try/main/pull"&&t.method==="POST"){const c=await B(t);if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).length)return h(e,400,{error:"Send an empty object."});try{const o=await fetch(`http://127.0.0.1:${process.env.KICKFLIP_HUB_PORT||4500}/_hub/pull`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({route:D.slice(1)||f.basename(d)}),signal:AbortSignal.timeout(3e4)});return h(e,o.status,await o.json())}catch(o){return h(e,502,{error:`Could not pull: ${o.message}`})}}if(a==="/api/try"&&t.method==="POST"){const c=await B(t);if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(o=>o!=="key")||typeof c.key!="string")return h(e,400,{error:"Send only the card key."});try{const o=await fetch(`http://127.0.0.1:${process.env.KICKFLIP_HUB_PORT||4500}/_hub/build`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({route:D.slice(1)||f.basename(d),key:c.key}),signal:AbortSignal.timeout(1e4)});return h(e,o.status,await o.json())}catch(o){return h(e,502,{error:`Could not start the build: ${o.message}`})}}if(a==="/api/settings"&&t.method==="POST"){const c=await B(t);let o;try{o=await Na(d,c)}catch(g){if(g instanceof E)return h(e,400,{error:g.message});throw g}const w=o.changed.filter(g=>g!=="idle_notification"),S=w.length?await $("settings",Object.fromEntries(w.map(g=>[g,o.settings[g]]))):null;return o.changed.includes("auto_merge")&&o.settings.auto_merge&&await $("check-github",{reason:"auto-merge"}),h(e,200,{settings:o.settings,id:S})}if(a==="/api/arch-review"&&t.method==="POST"){await B(t);try{return h(e,200,await pn())}catch(c){if(c instanceof E)return h(e,c.status||400,{error:c.message});throw c}}if(a==="/api/hide"&&t.method==="POST"){const c=await B(t);try{await Qt(d,String(c.id||""),c.hidden!==!1)}catch(o){if(o instanceof E)return h(e,400,{error:o.message});throw o}return h(e,200,{ok:!0})}if(a==="/_claude/events"&&t.method==="GET")return h(e,200,await en(n.searchParams.get("kind"),Math.min(500,Number(n.searchParams.get("limit")||100))));if(a==="/api/upload"&&t.method==="POST")return await bn(t,e);if(t.method!=="GET"&&t.method!=="HEAD"){e.writeHead(405).end();return}if(a.startsWith("/uploads/"))return await jn(t,e,decodeURIComponent(a.slice(9)));if(a==="/plan.html"){e.writeHead(302,{location:`notes.html${n.search}`,"cache-control":"no-store"}).end();return}if(a==="/index.html"){e.writeHead(302,{location:`./${n.search}`,"cache-control":"no-store"}).end();return}const r=Ma.has(a.slice(1)),i=r?null:await Sn(a),s=r?null:i?he:a==="/prototype.html"?null:await fe(decodeURIComponent(a));if(!r&&!s){e.writeHead(404).end();return}const l=r?".html":f.extname(s).toLowerCase(),k=r?null:await m.stat(s);let p=r?Buffer.from(La({touchIcon:await Zt()})):i?Buffer.from(await vn(i)):await m.readFile(s);!r&&l===".html"&&(p=(await Va(p.toString("utf8"))).replaceAll("{{title}}",de((await A()).title)).replaceAll("{{touchIcon}}",await Zt())),!r&&l===".html"&&(p=Buffer.from(await qa(p,a)));const y=l===".html"?`W/"${Ct.createHash("sha1").update(p).digest("base64url").slice(0,20)}"`:`W/"${nt(k)}"`,O=l!==".html"&&n.searchParams.get("v")===nt(k),N={"content-type":_(te,l)||"application/octet-stream","cache-control":O?"public, max-age=31536000, immutable":"no-cache",etag:y};if(ne(t.headers["if-none-match"],y)){e.writeHead(304,{etag:y,"cache-control":N["cache-control"],vary:"accept-encoding"}).end();return}return await ee(e,200,N,p)}catch(n){e.headersSent||(n.status===403?e.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}).end(n.message):e.writeHead(n.status||(n.code==="ENOENT"?404:400)).end())}});let jt=!1;async function ge(){if(!jt){jt=!0;try{const t=Date.now(),e=(await x()).items.filter(a=>a.heldUntil&&a.lane!=="done"&&Date.parse(a.heldUntil)<=t),n=e.length?await R(C):[];for(const{key:a,heldUntil:r}of e)await ha(d,a,r,{announce:!n.some(i=>i.kind==="card-unblocked"&&i.data?.key===a&&i.data?.until===r)})}catch(t){console.error("Pick-up check failed:",t.message)}finally{jt=!1}}}u(ge,"checkPickUps");let z=null,Pt=!1,U=!1;function lt(){return z?(Pt=!0,z):(z=(async()=>{do{Pt=!1;try{await In()}catch(t){console.error("Auto check failed:",t.message)}}while(Pt)})().finally(()=>{z=null}),z)}u(lt,"checkAuto");async function In(){const t=T(await A()),e=await R(C),{items:n,reviews:a}=await x(),{idle:r,pullable:i,waiting:s,starts:l,pulls:k,plannable:p}=We(n,{cap:t.max_implementers,pulled:await xt($t,e)});U=et().state==="paused";const y=r&&!U;for(const S of U?[]:l)await Et(await wa(d,S,g=>ut(d,S.split("/")[1],g)));if(t.headless_builders&&!U&&await Bn(),await Hn(t,e,n,y))return;const O=u(async S=>Et(await zt(d,S,g=>ut(d,S.split("/")[1],g),"hub")),"startPull");if(await ve($t,t.auto_pull,s,U?[]:k,t.headless_builders?O:null))return;const N=i.length?[]:p,c=(await xt(ke,e)).filter(S=>N.includes(S));if(await ve(ke,t.auto_pull,c,y&&!c.length?N.slice(0,1):[]))return;const{due:o,repo:w}=a;y&&o&&await $("pr-review",{pr:o.number,head:o.head,repo:w,title:o.title,author:o.author,url:o.url,touches:o.touches,asked:!!o.asked})}u(In,"runAuto");async function Et(t){if(!t||t.queued)return null;if(T(await A()).headless_builders&&t.kind==="claude")try{return et().state==="paused"?await Tt(t.key,{model:t.model,pid:null,relaunched:!1}):await Tt(t.key,{model:t.model,...await ht(d,t),relaunched:!1}),null}catch(e){await Z(d,t.key,{text:`Couldn't launch the builder headless (${e.message}), so the orchestrator launches it.`})}return $("card-build",t)}u(Et,"handOff");async function Tt(t,e){const n=f.join(v,"headless-builders.json"),a=await b(n,{});e?a[t]=e:delete a[t],await m.mkdir(v,{recursive:!0}),await m.writeFile(n+".tmp",JSON.stringify(a)+`
14
+ `),await m.rename(n+".tmp",n)}u(Tt,"trackLaunch");let At=!1;async function Dn(){if(!At){At=!0;try{if(!T(await A()).headless_builders||et().state==="paused")return;const t=f.join(v,"headless-builders.json"),e=await b(t,{});if(!Object.keys(e).length)return;const n=new Set(dt(await oe(await yt())).items.filter(s=>s.lane!=="done"&&s.builder&&!s.builder.planner).map(s=>s.key)),i=await na(e,n,{launch:u(async s=>{try{return await ht(d,s)}catch(l){return await Z(d,s.key,{text:`Couldn't relaunch the builder headless (${l.message}).`}),{pid:null}}},"launch"),handBack:u(async s=>{await Z(d,s.key,{text:"Its headless builder exited twice with the build unfinished, so the orchestrator launches it."}),await $("card-build",{...s,kind:"claude"})},"handBack")});JSON.stringify(i)!==JSON.stringify(e)&&(await m.writeFile(t+".tmp",JSON.stringify(i)+`
15
+ `),await m.rename(t+".tmp",t))}catch(t){console.error("Headless builder check failed:",t.message)}finally{At=!1}}}u(Dn,"reviveHeadless");async function Bn(){const t=f.join(v,"landers.json"),e=await b(t,{}),n=(await ua(d)).filter(a=>e[a.key]!==a.verified);if(n.length){for(const a of n){try{await ht(d,a,{lander:!0}),await Tt(a.key,null)}catch(r){await Z(d,a.key,{text:`Couldn't launch the lander headless (${r.message}); land it with board.mjs land ${a.key}.`})}e[a.key]=a.verified}await m.writeFile(t+".tmp",JSON.stringify(e)+`
16
+ `),await m.rename(t+".tmp",t)}}u(Bn,"launchLanders");async function ye(){const t=Xe(new Date,Ye(await b(f.join(Bt.homedir(),".claude","kickflip","settings.json"),{})));return t&&Ve(t)}u(ye,"triageNight");async function Hn(t,e,n,a){const r=f.join(v,"triaged.json"),i=await b(r,{}),{waiting:s,seen:l}=Qe(n),k=u(async()=>{await m.mkdir(v,{recursive:!0}),await m.writeFile(r+".tmp",JSON.stringify({seen:l})+`
17
+ `),await m.rename(r+".tmp",r)},"save"),p=e.filter(c=>c.kind==="overnight-triage").at(-1);if(p&&!(await rt(p)).reply)return i.seen!==l&&await k(),!0;if(!t.auto_triage||!a||s.length<2||l===i.seen)return!1;const y=await ye();if(!y||p?.data?.date===y)return!1;const O=`overnight-triage-${y}`;return((await b(f.join(v,"owner.json"),{topics:[]})).topics||[]).some(c=>c.id===O)||await ia(d,{id:O,title:`Overnight triage ${y}`}),await $("overnight-triage",{date:y,topic:O,cards:s}),await k(),!0}u(Hn,"checkTriage");async function Cn(){if(!T(await A()).auto_triage)return!1;const t=await ye();return!!t&&(await R(C)).filter(e=>e.kind==="overnight-triage").at(-1)?.data?.date!==t}u(Cn,"triageDue");const $t={kind:"card-pull",file:"pulled.json",line:"Pulled in: an implementer slot was free."},ke={kind:"card-plan",file:"planned.json",line:"Asked for a grill or prototype: nothing in Todo was ready to build."},Se=u(t=>t.keys||(t.key?[t.key]:[]),"markedOf");async function xt({kind:t,file:e},n){const a=await b(f.join(v,e),{}),r=String(a.at||""),i=n.filter(s=>s.kind===t&&String(s.ts)>r).map(s=>s.data?.key).filter(Boolean);return[...new Set([...Se(a),...i])]}u(xt,"sentOf");async function ve({kind:t,file:e,line:n},a,r,i,s=null){const l=f.join(v,e),k=a?[...r,...i]:[];for(const p of a?i:[]){if(s){await s(p);continue}await $(t,{key:p}),await m.mkdir(f.dirname(M(p)),{recursive:!0}),await m.appendFile(M(p),JSON.stringify({ts:new Date().toISOString(),who:"claude",text:n})+`
18
+ `)}return JSON.stringify(Se(await b(l,{})))!==JSON.stringify(k)&&(await m.mkdir(v,{recursive:!0}),await m.writeFile(l+".tmp",JSON.stringify({keys:k,at:new Date().toISOString()})+`
19
+ `),await m.rename(l+".tmp",l)),k.length>0}u(ve,"sendPulls");let K=null,Nt=!1;function _t(){return K?(Nt=!0,K):(K=(async()=>{do{Nt=!1;try{await Fn()}catch(t){console.error("Action push check failed:",t.message)}}while(Nt)})().finally(()=>{K=null}),K)}u(_t,"checkActions");async function Fn(){const t=f.join(v,"action-pushes.json"),e=await b(t,null),n=Me(await x()),a=Object.fromEntries(n.map(r=>[r.tag,{kind:r.kind,ids:r.questions.map(i=>i.id)}]));if(e){for(const i of n){const s=_(e,i.tag),l=i.questions.filter(p=>!s?.ids?.includes(p.id));if(s?.kind===i.kind&&!l.length)continue;const k=l.length?`questions.html#${encodeURIComponent(l[0].id)}`:i.url;try{await Kt(d,{kind:"action",title:i.title,body:l[0]?.question||i.body,url:`/${st}/${k}`,tag:i.tag,now:i.now})}catch(p){if(p instanceof E)console.error(`Push ${i.tag} not sent:`,p.message.split(`
20
+ `).at(-2)||p.message);else throw p}}const r=Object.keys(e).filter(i=>!_(a,i));r.length&&await qe(r,st)}JSON.stringify(a)!==JSON.stringify(e)&&(await m.mkdir(v,{recursive:!0}),await m.writeFile(t+".tmp",JSON.stringify(a)+`
21
+ `),await m.rename(t+".tmp",t))}u(Fn,"pushActions");let Q=null,Rt=!1;function It(){return Q?(Rt=!0,Q):(Q=(async()=>{do{Rt=!1;try{await Jn()}catch(t){console.error("Idle check failed:",t.message)}}while(Rt)})().finally(()=>{Q=null}),Q)}u(It,"checkIdle");async function Jn(){const t=f.join(v,"idle.json"),e=await A();if(!T(e).idle_notification)return m.rm(t,{force:!0});const n=await b(t,{}),a=await x(),r=ze(a,{pulled:await xt($t,await R(C))}),{state:i,push:s}=He(n,{idle:r,now:new Date});if(s)try{await Kt(d,{kind:"reminder",title:`${e.title||st} has gone idle`,body:Ce(a.items,n.last),url:`/${st}/`,tag:"idle",now:!0})}catch(l){if(l instanceof E)console.error("Idle push not sent:",l.message.split(`
22
+ `).at(-2)||l.message);else throw l}JSON.stringify(i)!==JSON.stringify(n)&&(await m.mkdir(v,{recursive:!0}),await m.writeFile(t+".tmp",JSON.stringify(i)+`
23
+ `),await m.rename(t+".tmp",t))}u(Jn,"stepIdle");let V=null,Dt=!1;function be(){return V?(Dt=!0,V):(V=(async()=>{do{Dt=!1;try{await Un()}catch(t){console.error("Done archive failed:",t.message)}}while(Dt)})().finally(()=>{V=null}),V)}u(be,"checkArchive");async function Un(){if(!Ft(await x()).length)return;const t=Ft(await W()),e=t.length?await ra(d,t.map(n=>({key:n.key,closed_at:n.sortAt}))):[];e.length&&console.log(`Done archive: ${e.length} card${e.length===1?"":"s"} moved`)}u(Un,"archiveDone");try{for(const t of await da(d))console.log(t)}catch(t){console.error("follow-up migration:",t.message)}try{await fa(d)}catch(t){console.error("dead-field strip:",t.message)}await be(),await _n(),setInterval(()=>{H++,St(),Dn(),ge(),U&&et().state!=="paused"?lt():Cn().then(t=>{t&&lt()},t=>console.error("Triage check failed:",t.message)),_t(),It()},6e4).unref(),await St(),await ge(),await lt(),_t(),It(),Rn.listen(qt,"127.0.0.1",()=>console.log(`site on http://127.0.0.1:${qt}/ (prefix ${D||"none"})`));