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