@lovek4d/kickflip 0.6.0 → 0.6.1

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 (41) hide show
  1. package/CONTEXT.md +15 -11
  2. package/SKILL.md +7 -7
  3. package/bin/doctor.mjs +5 -4
  4. package/bin/install.mjs +3 -3
  5. package/board/bin/board.mjs +26 -12
  6. package/board/domain.mjs +7 -7
  7. package/board/headless.mjs +9 -9
  8. package/board/public/app-cards.js +23 -44
  9. package/board/public/app-lanes.js +4 -7
  10. package/board/public/app-threads.js +12 -8
  11. package/board/public/prototype-switcher.js +7 -2
  12. package/board/public/shell.js +4 -3
  13. package/board/public/sidebar.css +5 -0
  14. package/board/public/sidebar.js +3 -0
  15. package/board/public/style.css +11 -19
  16. package/board/public/ui-cards.js +21 -15
  17. package/board/public/ui-enums.js +2 -2
  18. package/board/public/ui-header.js +4 -6
  19. package/board/public/ui-hub.js +7 -4
  20. package/board/public/ui-order.js +1 -1
  21. package/board/public/ui-page-header.js +1 -1
  22. package/board/public/ui-sidebar.js +11 -5
  23. package/board/public/ui-snapshot.js +1 -1
  24. package/board/public/ui.js +3 -2
  25. package/board/public/view-hub.js +3 -1
  26. package/board/public/view-questions.js +1 -1
  27. package/board/server.mjs +20 -19
  28. package/board/stop.mjs +1 -1
  29. package/board/store.mjs +27 -25
  30. package/board/transcript.mjs +2 -2
  31. package/hooks-plugin/hooks/board-data-gate.mjs +9 -6
  32. package/hub/front-door.mjs +1 -1
  33. package/hub/hub.mjs +1 -1
  34. package/hub/sessions.mjs +2 -2
  35. package/hub/try.mjs +7 -7
  36. package/hub/usage.mjs +3 -3
  37. package/package.json +1 -1
  38. package/shared/card-prs.mjs +1 -1
  39. package/shared/machine-gate.mjs +2 -2
  40. package/shared/tailscale.mjs +1 -1
  41. package/shared/usage-gate.mjs +1 -1
@@ -60,9 +60,10 @@
60
60
  // A composer's send buttons: the one with nth 0 presses on ⌘↵ and the one with nth 1, where there is one, on ⌥↵,
61
61
  // wherever each sits in the row (Ctrl+↵ and
62
62
  // Alt+↵ off a Mac), each with its key as a small gray hint that style.css shows only where there is a keyboard and room.
63
+ // tone is its class: primary for the nth 0 one unless given, as resolves on the Send or Reply that clears Action needed.
63
64
  const MAC = /Mac|iPhone|iPad/.test(globalThis.navigator?.platform || globalThis.navigator?.userAgent || '');
64
65
  const SEND_KEYS = MAC ? ['⌘↵', '⌥↵'] : ['Ctrl+↵', 'Alt+↵'];
65
- const sendButton = (label, nth, attrs = '') => `<button type="submit"${nth === 0 ? ' class="primary"' : ''} data-nth="${nth}"${attrs}>${esc(label)}<span class="key-hint" aria-hidden="true">${SEND_KEYS[nth]}</span></button>`;
66
+ const sendButton = (label, nth, attrs = '', tone = nth === 0 ? 'primary' : '') => `<button type="submit"${tone ? ` class="${tone}"` : ''} data-nth="${nth}"${attrs}>${esc(label)}<span class="key-hint" aria-hidden="true">${SEND_KEYS[nth]}</span></button>`;
66
67
  // The send button an Enter keydown in form's box presses, or null.
67
68
  const keyedSend = (ev, form) => {
68
69
  if (ev.key !== 'Enter' || ev.isComposing) return null;
@@ -379,7 +380,7 @@
379
380
  removed: { line: 'This board is gone', hint: 'The hub no longer lists it.', retries: false },
380
381
  access: { line: 'You no longer have access', hint: 'Ask its owner to share it again.', retries: false },
381
382
  erroring: { line: 'Board restarting', hint: 'It answers with errors. Retrying for 30 seconds.' },
382
- unreachable: { line: 'Board offline', hint: 'The hub, your connection or the Mac is down. Retrying.' },
383
+ unreachable: { line: 'Board offline', hint: 'The hub, your connection or the host is down. Retrying.' },
383
384
  };
384
385
  const RESTART_WHY = { code: 'Its code changed.', starting: 'It’s starting.', crashed: 'It stopped, and the hub is starting it again.' };
385
386
  function downOf(answer, online = navigator.onLine !== false) {
@@ -184,7 +184,9 @@ ${integrationsHtml(d.integrations)}${sitesHtml(d.groups, restarting)}${d.remembe
184
184
  answer(out, action) {
185
185
  if (['/_hub/settings/codex', '/_hub/settings/claude-model', '/_hub/settings/pause-at', '/_hub/settings/keep-going'].includes(action)) {
186
186
  last = out; flash = null; paint();
187
- if (action.endsWith('/keep-going')) B.toast(out.pause.state === 'going' ? `Keep going: work runs past the buffer until ${BoardUI.usageClock(out.pause.resets_at)}.` : 'Stopped: new work waits again.');
187
+ if (action.endsWith('/keep-going')) B.toast(out.pause.state === 'going' ? `Keep going: work runs past the buffer until ${BoardUI.usageClock(out.pause.resets_at)}.`
188
+ : out.pause.near?.going ? `Keep going: agents start in parallel until ${BoardUI.usageClock(out.pause.near.resets_at)}.`
189
+ : out.pause.near ? 'Stopped: agents start one at a time again.' : 'Stopped: new work waits again.');
188
190
  } else if (action === '/_hub/push/flush') { flash = { text: out.message, error: !out.ok }; load(); } else { flash = null; load(); settings.B.refreshSettings?.(); }
189
191
  ctx.sidebarChanged?.();
190
192
  },
@@ -11,7 +11,7 @@
11
11
  // The question then reads prototype requested, locked, until its prototype round arrives, and once the request is
12
12
  // recorded the page moves on as a Send does (docs/adr/2026-11-03-1803-a-question-asked-for-a-prototype-is-resolved.md,
13
13
  // docs/adr/2026-11-04-2130-a-prototype-request-waits-for-its-prototype.md).
14
- // A question the owner has sent an answer to is resolved: read-only, green, with a Resolved badge. One that resolves
14
+ // A question the owner has sent an answer to is resolved: read-only, with a Resolved badge. One that resolves
15
15
  // while its topic shows stays in place until you leave that topic, by a pill, a link or the move on after the last
16
16
  // Send; a topic's sent questions otherwise fold under "resolved (N)", which names only the count, above its open ones. A sent question's badge, once confirmed, sends `question-reopen {key?, question}`, and the question
17
17
  // opens again with its last pick and note for a new Send. Each question is numbered by its place on its card
package/board/server.mjs CHANGED
@@ -1,24 +1,25 @@
1
1
  #!/usr/bin/env node
2
- var aa=Object.defineProperty;var u=(t,e)=>aa(t,"name",{value:e,configurable:!0});async function ee(t,e){let a;try{a=await k.open(w.join(t,"data","try",`${e}.log`),"r");const{size:n}=await a.stat(),s=Math.max(0,n-64*1024),r=Buffer.alloc(n-s),{bytesRead:d}=await a.read(r,0,r.length,s),l=r.subarray(0,d).toString("utf8").split(/\r?\n/);return s&&l.shift(),l.at(-1)===""&&l.pop(),l.slice(-40)}catch(n){if(n.code==="ENOENT")return[];throw n}finally{await a?.close()}}u(ee,"tryLogTail");import{topicItems as na}from"./review.mjs";import{boardWorktree as ut,builderRepo as ia,helperWorktree as ra,moveQueuedLand as sa,prWorktree as oa}from"./worktree.mjs";import{transcriptMovedAt as ca,transcriptCommandRunning as la}from"./transcript.mjs";import{fromHost as ua}from"./on-host.mjs";import{readSync as da,writeSync as fa,stamp as ha,patchSince as wa}from"./sync.mjs";import{archOf as pa,readReviews as ma,repoName as ga,startReview as ya}from"./arch-review.mjs";import ka from"node:http";import k from"node:fs/promises";import gt from"node:os";import w from"node:path";import{execFile as ae,spawn as ba}from"node:child_process";import{fileURLToPath as va}from"node:url";import ne from"node:vm";import Sa from"node:zlib";import Oa from"node:crypto";import{promisify as ja}from"node:util";import{archivedOf as ie,buildBoard as yt,movesOf as $a,visualsDue as Pa,withHub as Aa,DONE_KEPT as re,ORCHESTRATOR_PUSH_AFTER as Ea,orchestratorWaits as Ta,waitsStep as Ra,PROTOTYPE_WAIT as Ia,prototypeWaits as xa,idleStep as _a,idleSummary as Na,intakeDue as Da,intakeStranded as se,ITEM_EVENTS as Ca,QUESTION_REOPEN as kt,lineupMoved as Ha,slimDone as oe,needsAnswer as Ba,ownerActions as Fa,picksNoTake as Ma,pullOf as La,arrivalsOf as Ua,rebasesDue as Wa,refOf as bt,roundsDue as Ja,strictlyIdle as Ga,threadOf as Ka,triageOf as za}from"./domain.mjs";import{localDay as Va,quietMiddleOf as Qa,quietOf as Ya}from"../hub/schedule.mjs";import{cardPRs as Xa,prKeyOf as ce}from"../shared/card-prs.mjs";import{dropQueued as Za}from"../hub/push.mjs";import{closeSync as qa,createReadStream as tn,existsSync as le,mkdirSync as en,openSync as an,readdirSync as nn,statSync as ue,watch as de}from"node:fs";import{PROTOTYPE_TEMPLATE as rn,esc as sn,prototypePage as on,versionOf as vt}from"./prototype-page.mjs";import{BOARD_CLI as cn,launchBuilder as G,launchIntake as ln,pidAlive as St,reviveBuilders as un,stopAgents as dn}from"./headless.mjs";import{BoardError as A,intakeHandoff as fe,quickAsk as fn,startHelper as hn,startVisual as wn,openAgents as pn,isLanding as mn,implementAsIs as gn,stageImplement as yn,stageReply as kn,planAgain as bn,heldMerge as vn,opensPRs as Sn,tapMerge as On,addTopic as jn,appendThread as N,archiveCards as $n,archiveCount as Pn,archiveRank as An,readArchive as he,boardConfig as En,changeLineup as Tn,abandonLaunch as Rn,cutOffBuilds as In,abandonPlanner as dt,landable as xn,relaunchLander as _n,migrateFollowups as Nn,migrateStatuses as Dn,moveCard as Cn,stageImplementAsks as Hn,startPlanner as Bn,stripDeadFields as Fn,pickUp as Mn,startRebase as Ln,startTappedBuild as Ot,sendPush as Un,recordOwnerTap as Wn,takeHoldStopped as Jn,mergeCard as Gn,readPrototypes as Kn,finishHandshake as zn,itemSources as Q,readAskLinks as jt,readAskProgress as Vn,readBoardRules as Qn,readBoardSettings as Yn,readHandshake as Xn,readHidden as Zn,readLineupNotes as qn,readMarks as ti,readNotes as ei,readWorking as $t,readUpload as ai,addReviewers as ni,removeReviewers as ii,markRead as we,resolveNoteAnswers as ri,settleAnswers as si,unrecorded as oi,saveBoardRules as ci,saveBoardSettings as li,saveBoardShare as ui,saveUpload as di,setBoardPause as fi,setHidden as pe,settingsOf as D,threadFile as hi,wavesDir as wi}from"./store.mjs";import{changed as pi,readHubRules as mi}from"../hub/rules.mjs";import{boardAt as K,listenState as gi,linksOf as yi}from"../hub/boards.mjs";import{pendingFor as ki,shown as bi}from"../hub/prompts.mjs";import{routeRecord as W}from"../hub/routes.mjs";import{CHOICES as vi,FOREVER as me,durationEnd as ge}from"../hub/duration.mjs";import{VIEW_PAGES as Si,shellHtml as Oi}from"../hub/shell.mjs";import{crossSite as ji,isJSON as $i}from"../starter/cross-site.mjs";import{shareEntries as Pi}from"../hub/share.mjs";import{readUsageGate as Y,heldFor as ye,holdsWork as Pt,pauseReasonFor as Ai,boardPausedUntil as Ei,MACHINE_HELD_LINE as Ti}from"../shared/usage-gate.mjs";import{audit as Ri,boardAudit as Ii}from"../shared/audit.mjs";import{reviewWaits as xi}from"../shared/skills.mjs";import{boardRepo as At,mainRefOf as _i}from"../hub/board-repo.mjs";import{listBuildTargets as Ni}from"./build-script.mjs";import{permissionsView as Di,setPermission as Ci}from"./permissions.mjs";import{SlackError as ke,SLACK_NEEDS_TOKEN as be,savedSlack as ve,slackStatus as Hi}from"../hub/slack.mjs";import{collaborators as Bi,githubUser as Fi}from"../hub/pr-report.mjs";const X=w.dirname(va(import.meta.url));async function Se(){const t=await k.stat(w.join(X,"..","hub","icon-180.png")).catch(()=>null);return`/_hub/icon-180.png?v=${t?Math.floor(t.mtimeMs).toString(36):"0"}`}u(Se,"touchIcon");async function Mi(t){if(!t.includes("<!--header "))return t;const e=ne.createContext({});return ne.runInContext(await k.readFile(w.join(X,"public","ui-page-header.js"),"utf8"),e),t.replace(/<!--header (\{.*?\})-->/g,(a,n)=>e.BoardUI.header(JSON.parse(n)))}u(Mi,"pageHeaders");const o=w.resolve(process.argv[2]||process.cwd()),Li=[w.join(o,"public"),w.join(X,"public"),w.join(X,"..","starter","public"),w.join(X,"..","shared")],O=w.join(o,"data"),E=u(t=>Ri(Ii(o),t),"logCall"),C=w.join(O,"events.jsonl"),ft=w.join(O,"replies"),Oe=Number(process.env.PORT||4600),J=(process.env.PREFIX||"").replace(/\/+$/,""),je={".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"},Ui=ja(Sa.gzip),Wi=/^(text\/|application\/json|image\/svg\+xml)/;async function $e(t,e,a,n){!["GET","HEAD"].includes(t.req?.method)&&!t.req.boardRead&&x++;const s={...a};let r=Buffer.isBuffer(n)?n:Buffer.from(n??"");if(Wi.test(s["content-type"]||"")&&(s.vary="accept-encoding",r.length>=1024&&/\bgzip\b/.test(t.req?.headers["accept-encoding"]||"")))try{r=await Ui(r),s["content-encoding"]="gzip"}catch{}s["content-length"]=r.length,t.writeHead(e,s),t.end(t.req?.method==="HEAD"?void 0:r)}u($e,"send");const h=u((t,e,a,n={})=>$e(t,e,{"content-type":je[".json"],"cache-control":"no-store",...n},JSON.stringify(a)),"json"),Pe=/\b(src|href)="([\w.][\w./-]*\.(?:js|css))"/g;async function Ji(t,e){const a=e.endsWith("/")?e:w.posix.dirname(e),n=[...new Set([...t.matchAll(Pe)].map(r=>r[2]))],s=new Map(await Promise.all(n.map(async r=>{const d=await He(w.posix.join(a,r)),l=d?await k.stat(d).catch(()=>null):null;return[r,l?vt(l):null]})));return t.replace(Pe,(r,d,l)=>s.get(l)?`${d}="${l}?v=${s.get(l)}"`:r)}u(Ji,"versionAssets");const Ae=u((t,e)=>String(t||"").split(",").map(a=>a.trim().replace(/^W\//,"")).includes(e.replace(/^W\//,"")),"sameTag"),j=u(async(t,e)=>{try{return JSON.parse(await k.readFile(t,"utf8"))}catch{return e}},"readJSON"),F=u(async t=>{try{return(await k.readFile(t,"utf8")).split(`
3
- `).filter(Boolean).map(e=>JSON.parse(e))}catch{return[]}},"readLines"),Z=u(t=>hi(o,t),"threadFile"),Et=u(async()=>{const t=await Zn(o);return(await F(C)).filter(e=>!t.has(e.id))},"shownEvents");async function H(t,e=64*1024){if(!$i(t.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});let a="";for await(const n of t)if(a+=n,a.length>e)throw Object.assign(new Error("too large"),{status:413});return a?JSON.parse(a):{}}u(H,"readBody");const Gi=new Set(["owner-answer","owner-answers","prototype-request","reask-request",kt,"grill-more","visual-request","item-message","ask","ask-edited","ask-withdrawn","ask-bump","ask-done","card-build","card-hold","card-done","card-bump","card-unbump","builder-nudge","lineup-move","plan-waves"]),Ki="This board is shared with you to look at only.",zi="Merge and Board settings stay the owner\u2019s.";async function Vi(t,e){const a=t.headers["x-kickflip-shared"];return a?Pi(await B()).find(s=>s.login===a)?.level!=="full"||!["/_claude/events","/api/upload"].includes(e)||t.method!=="POST"?{refused:Ki}:{by:{login:a,name:t.headers["tailscale-user-name"]||null}}:null}u(Vi,"sharedWriter");const M=u(async(t,e)=>(await Tt([[t,e]]))[0],"appendEvent");async function Tt(t){const e=new Date().toISOString(),a=t.map(([n,s])=>({id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,7),ts:e,kind:n,data:s}));return await k.mkdir(ft,{recursive:!0}),await k.appendFile(C,a.map(n=>JSON.stringify(n)+`
4
- `).join("")),x++,Me=Le(C),a.map(n=>n.id)}u(Tt,"appendEvents");async function Ee(t){const e=await oi(o,t),a=new Map(((await j(w.join(O,"owner.json"),{items:[]})).items||[]).map(d=>[d.id,d]));if(!e.length)return[];const n=u(d=>a.has(String(d.id))&&Ma(a.get(String(d.id)),d.choice),"again"),s=await Tt(e.map(d=>["owner-answer",n(d)?{...d,prototype_again:!0}:d]));await ri(o,e);const{prototype:r}=await si(o,e);x++;for(const d of r)await it(d,"prototype");return s}u(Ee,"recordAnswers");async function Qi(t){if(!Array.isArray(t)||!t.length)return"Send at least one answer.";const e=new Set(((await j(w.join(O,"owner.json"),{items:[]})).items||[]).map(n=>n.id)),a=t.filter(n=>!e.has(String(n?.id)));return a.length?`${a.map(n=>String(n?.id)).join(", ")} ${a.length===1?"is":"are"} no longer asked here. Reload to see what is, then send again.`:null}u(Qi,"answersRefused");async function Yi(t){const e=(await R()).owner.find(a=>a.id===t);return e?["open","requested"].includes(e.state)?`${t} has no answer sent, so there is nothing to reopen.`:null:`${t} is no longer asked here. Reload to see what is.`}u(Yi,"reopenRefused");async function Xi(t){const e=(await R()).owner.find(a=>a.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(Xi,"reaskRefused");async function ht(t){for(const e of["html","json"]){const a=w.join(ft,`${t.id}.${e}`);try{const[n,s]=await Promise.all([k.readFile(a,"utf8"),k.stat(a)]);return{...t,reply:{[e]:e==="json"?JSON.parse(n):n,at:s.mtime.toISOString()}}}catch{}}return{...t,reply:null}}u(ht,"withReply");async function Zi(t,e){const a=await $t(o);let n=await Et();return t&&(n=n.filter(s=>s.kind===t)),Promise.all(n.reverse().slice(0,e).map(async s=>{const r=await ht(s);return r.reply||!W(a,s.id)?r:{...r,working:W(a,s.id)}}))}u(Zi,"listEvents");const Rt=new Map,qi="number,title,url,state,isDraft,reviewDecision,mergeStateStatus,mergeable,statusCheckRollup,mergedAt,headRefName,headRefOid,updatedAt,latestReviews";function tr(t,e){return new Promise(a=>{ae("gh",["pr","view",String(e),"--repo",t,"--json",qi],{timeout:2e4},(n,s)=>{if(n)return a(null);try{a(JSON.parse(s))}catch{a(null)}})})}u(tr,"ghPR");function er(t,e){return new Promise(a=>{ae("gh",["api",`repos/${t}/pulls/${e}/comments?per_page=100`],{timeout:2e4},(n,s)=>{if(n)return a(0);try{const r=JSON.parse(s),d=new Set(r.map(l=>l.in_reply_to_id).filter(Boolean));a(r.filter(l=>!l.in_reply_to_id&&/codex/i.test(l.user?.login||"")&&!d.has(l.id)).length)}catch{a(0)}})})}u(er,"ghCodexOpen");function ar(t){const e=t||[];let a=0,n=0;for(const s of e){const r=(s.conclusion||s.state||"").toUpperCase(),d=(s.status||"").toUpperCase();["FAILURE","ERROR","CANCELLED","TIMED_OUT","ACTION_REQUIRED","STARTUP_FAILURE"].includes(r)?a++:(d&&d!=="COMPLETED"||r==="PENDING"||r==="EXPECTED")&&n++}return{total:e.length,fail:a,pending:n,state:a?"fail":n?"pending":e.length?"pass":"none"}}u(ar,"checksOf");async function nr(t,e){const a=`${t}#${e}`;let n=Rt.get(a);return n||(n={at:0,value:null,loading:null},Rt.set(a,n)),Date.now()-n.at>45e3&&!n.loading&&(n.loading=Promise.all([tr(t,e),er(t,e)]).then(([s,r])=>{if(s){const{statusCheckRollup:d,latestReviews:l,...m}=s,y=Object.fromEntries((l||[]).map(g=>[g.author?.login,g.state])),v={...m,checks:ar(d),reviews:y,codexOpen:r};JSON.stringify(v)!==JSON.stringify(n.value)&&x++,n.value=v}n.at=Date.now(),n.loading=null})),!n.value&&n.loading&&await n.loading,n.value}u(nr,"prState");const B=u(()=>En(o),"config");async function ir(){const t=await k.readdir(ft).catch(()=>[]),e=await Promise.all(t.map(n=>k.stat(w.join(ft,n)).then(s=>s.mtimeMs,()=>0))),a=Math.max(0,...e);return a?new Date(a).toISOString():null}u(ir,"latestReply");async function Te(t){let e=[];try{e=(await k.readdir(t)).filter(s=>s.endsWith(".json"))}catch{}const a=await Promise.all(e.map(s=>j(w.join(t,s),null))),n=Object.create(null);return e.forEach((s,r)=>{if(!a[r])return;const[d,l="main"]=w.basename(s,".json").split("@");(n[d]||=Object.create(null))[l]=a[r]}),n}u(Te,"readAgents");async function It(){const t=K(o);return{cfg:await B(),github:t?.github,local:t?.local,hub:t?.hub,remote:t?.remote,branch:t?.branch,topicItems:na()}}u(It,"outsideData");async function Re(){return(await Promise.all((await Q(o)).map(async t=>{const e=await j(t.file,null);return!e||!Array.isArray(e.items)?null:{slug:t.slug,wave:e,agents:await Te(w.join(t.dir,"agents"))}}))).filter(Boolean)}u(Re,"liveWaves");async function rr(t){const e=await Re();for(const a of t){const n=e.find(s=>s.slug===a.wave);n?n.wave.items.push(a.item):e.push({slug:a.wave,wave:{items:[a.item]},agents:{}})}return e}u(rr,"withArchived");async function Ie({cfg:t,github:e,local:a,hub:n,remote:s,branch:r},d){const l=t.items==="waves";d??=await Re();const m=Object.create(null);return await Promise.all(d.flatMap(({slug:y,wave:v})=>v.items.map(async g=>{m[`${y}/${g.id}`]=await F(Z(`${y}/${g.id}`))}))),{github:e,remote:s,branch:r,reviewers:Sn({github:e,local:a,hub:n},D(t,{hub:n}))?t.reviewers||[]:[],fromWaves:l,autoMerge:D(t).auto_merge,skipDraft:D(t).skip_draft,autoBuild:D(t).auto_build,tickets:D(t).tickets,waves:d,threads:m,working:await $t(o),autoReview:D(t).auto_review,incoming:await j(w.join(O,"incoming.json"),null),reviewRecords:await j(w.join(O,"reviews.json"),{})}}u(Ie,"itemSnapshot");async function xe(t,e=[]){const a=await Ie(t,e.length?await rr(e):void 0),{waves:n,threads:s,working:r}=a,d=Object.create(null),l=new Set(e.map(c=>c.key));await Promise.all(n.flatMap(({slug:c,wave:f})=>{const $=a.github||f.repo;return f.items.flatMap(p=>Xa(p,$).map(async S=>{d[ce(S)]=l.has(`${c}/${p.id}`)?Rt.get(ce(S))?.value??null:await nr(S.repo,S.number)}))}));const m=new Set(["ask","owner-answer","grill-more","visual-request","item-message","wave-triage","overnight-triage"]),y=await Promise.all((await Et()).map(c=>m.has(c.kind)||W(r,c.id)?ht(c):{...c,reply:null}));await Promise.all(y.filter(c=>c.kind==="ask").map(async c=>{const f=await F(Z(`ask:${c.id}`));f.length&&(s[`ask:${c.id}`]=f)}));const v=Object.create(null),g=Object.create(null);for(const{agents:c}of n)for(const f of Object.values(c||{}).flatMap($=>Object.values($))){const $=f.worktree?.path,p=$&&!f.done?ca($):null;p!==null&&(v[$]=p),$&&!f.done&&la($)&&(g[$]=!0)}const b=Object.create(null),T=await j(w.join(O,"headless-builders.json"),{}),i=await j(w.join(O,"landers.json"),{});for(const[c,f]of[...Object.entries(i),...Object.entries(T)])f?.pid===null&&W(T,c)===f&&(b[c]="held"),Number.isInteger(f?.pid)&&(St(f.pid)?b[c]=!0:f.handedBack?b[c]=!1:delete b[c]);return{...a,moved:v,commandRunning:g,alive:b,try:await j(w.join(O,"try.json"),null),main:await j(w.join(O,"main.json"),null),prs:d,events:y,askLinks:await jt(o),askProgress:await Vn(o),checked:await j(w.join(O,"checked.json"),null),watcher:await j(w.join(O,"watcher.json"),null),replyAt:await ir(),owner:(c=>({...c,items:[...c.items||[],...e.flatMap(f=>f.questions||[])]}))(await j(w.join(O,"owner.json"),{items:[]})),topicItems:t.topicItems,prSlack:await j(w.join(O,"pr-slack.json"),{})}}u(xe,"snapshot");async function sr(t){if(!t.length)return[];const e=new Map(yt(await xe(await It(),t)).items.map(a=>[a.key,a]));return t.map(a=>{const n=e.get(a.key);return oe({...n,sortAt:a.closed_at,worktree:n.worktree&&!le(n.worktree)?null:n.worktree})})}u(sr,"archivedCards");const z=J.slice(1)||w.basename(o);let x=0,q=null;async function R(){const t=await It(),e=JSON.stringify([x,t]);if(q?.key!==e){const a=xe(t).then(yt);q={key:e,built:a},a.catch(()=>{q?.built===a&&(q=null)})}return q.built}u(R,"sharedBoard");let xt=!1;async function _e(){if(!xt){xt=!0;try{const t=await F(C),e=new Set(t.filter(a=>a.kind==="prototype-overdue").map(a=>a.data?.request));for(const a of xa(await R(),t))e.has(a.request)||Date.now()-Date.parse(a.since)<Ia||await M("prototype-overdue",a)}catch(t){console.error("Prototype check failed:",t.message)}finally{xt=!1}}}u(_e,"checkPrototypeWaits");async function or(t){const e=await Xn(o);if(!e?.ping?.id||e.done_at)return null;const a=t?.find(s=>s.link==="github"),n={events:e.ack?.id===e.ping.id,...a||!t?{github:!!a?.ok}:{}};return n.events&&n.github!==!1?(await zn(o),null):n}u(or,"setupOf");const Ne=vi.map(([t,e])=>({value:t,label:e,ms:t===me?null:ge(new Date(0),t)-new Date(0)}));async function cr(){const t={slack:null,github:[]},e=ve();if(!e||Hi().refused)t.slackProblem=be;else try{t.slack=await e.people()}catch(n){if(!(n instanceof ke))throw n;t.slackProblem=n.message==="Slack: missing_scope"?"Slack needs the users:read scope; update the app on Hub settings \u203A Slack.":n.message}const a=K(o)?.github;if(a)try{t.github=await Bi(a)}catch(n){t.githubProblem=`GitHub: ${n.message}`}return t}u(cr,"reviewerPeople");async function lr(t){const e=ve();if(!e)throw new A(be);return Promise.all(t.map(async a=>{const n=await Fi(a.github.trim());if(!n)throw new A(`GitHub has no user ${a.github.trim()}.`);return{name:a.name.trim(),slack_user:a.slack_user.trim(),dm:await e.openDM(a.slack_user.trim()),github:n}}))}u(lr,"withDMs");async function tt(){const t=new Date().toISOString(),{title:e,reviewers:a}=await B(),n=K(o),s=n?gi(n):"deaf",r=Aa(await R(),{listen:s,prompts:n?ki(n.route).map(bi):[]});r.items=r.items.map(l=>l.worktree&&!le(l.worktree)?{...l,worktree:null}:l);const d=n?yi(n,{listen:s}):null;return{generatedAt:t,title:e,reviewers:(a||[]).map(l=>l?.name).filter(l=>typeof l=="string"),reviewerGithub:Object.fromEntries((a||[]).filter(l=>typeof l?.name=="string").map(l=>[l.name,l.github])),...r,links:d,setup:await or(d),holdChoices:Ne,arch:n?pa(await ma(o),r.items,r.owner,{repo:ga(At(n.route)),waits:xi()}):null}}u(tt,"board");async function ur(t){["GET","HEAD"].includes(t.method)||x++;const e={...await tt(),doneArchived:await Pn(o)};e.owner=e.owner.filter(s=>s.state!=="resolved");const a=await ti(o);e.items=e.items.map(s=>({...s.lane==="done"?oe(s):s,readAt:a[s.key]||null}));const n=new Set(e.items.map(s=>s.key));return e.ownerTopics=e.ownerTopics.map(({title:s,summary:r,askedAt:d,...l})=>n.has(l.card)?l:{...l,title:s,summary:r,...d?{askedAt:d}:{}}),e}u(ur,"buildPageBoard");const _t=w.join(O,"sync.json");let V=await da(_t),Nt=V.n,Dt=Promise.resolve(),wt=null;function dr(){return Dt=Dt.then(async()=>{const t=V;t.n!==Nt&&(await k.mkdir(O,{recursive:!0}),await fa(_t,t),Nt=t.n)}),Dt}u(dr,"saveSync");async function fr(t){const e=await ur(t);for((!wt||e.generatedAt>=wt.generatedAt)&&({sync:V,board:wt}=ha(V,e));Nt<V.n;)await dr();return{sync:V,board:wt}}u(fr,"pageBoard");async function L(t,e){if(new URL(t.url,"http://site").searchParams.get("board")==="0")return;const a=await ua(t),{sync:n,board:s}=await fr(t);return t.boardRead=!0,{...(/^\d+$/.test(e)?wa(n,s,Number(e)):null)||s,onHost:a}}u(L,"answerBoard");let De=Promise.resolve();function hr(){const t=De.then(async()=>{const e=K(o);if(!e)throw Object.assign(new A("This board isn\u2019t in the hub\u2019s manifest."),{status:404});const{arch:a}=await tt();if(a.last?.open)return{key:a.last.key,ref:a.last.ref,existing:!0};const{key:n,data:s}=await ya(o,e),r=await M("arch-review",s);return{key:n,id:r,existing:!1}});return De=t.catch(()=>{}),t}u(hr,"runReview");async function wr(t){const[e,a]=t.split("/"),n=(await Q(o)).find(l=>l.slug===e),s=n?await j(n.file,null):null,r=!s?.items?.some(l=>l.id===a)&&(await he(o)).find(l=>l.key===t),d=Array.isArray(s?.items)&&s.items.find(l=>l.id===a)||r?.item||null;return{item:d,questions:r?.questions||[],agents:d&&n?W(await Te(w.join(n.dir,"agents")),a)||{}:{}}}u(wr,"itemOf");async function pr(t){const e=W(await jt(o),t),{tickets:a}=D(await B()),n=[];for(const s of await Q(o)){const r=await j(s.file,null);for(const d of Array.isArray(r?.items)?r.items:[]){const l=`${s.slug}/${d.id}`;d.from_ask===t?n.unshift({key:l,ref:bt(r,d,a),became:!0}):l===e&&n.push({key:l,ref:bt(r,d,a),became:!1})}}return n}u(pr,"askLinksOf");async function Ce(t){const{item:e,agents:a,questions:n}=t.startsWith("ask:")?{item:null,agents:{},questions:[]}:await wr(t),s=await j(w.join(O,"owner.json"),{items:[]}),r={...s,items:[...s.items||[],...n]},d=new Set(r.items.filter(i=>i.topic===`item:${t}`).map(i=>i.id)),l=t.startsWith("ask:"),m=l?t.slice(4):e?.from_ask||null,y=l?[]:Object.entries(await jt(o)).filter(([,i])=>i===t).map(([i])=>i),v=[m,...y].filter(Boolean),g=new Set([t,...v.map(i=>`ask:${i}`)]),b=(await Et()).filter(i=>g.has(i.data?.key)&&Ca.includes(i.kind)||i.kind==="owner-answer"&&d.has(i.data?.id)||i.kind===kt&&d.has(i.data?.question)||i.kind==="ask"&&v.includes(i.id)||i.kind==="ask-edited"),T=(await Promise.all([...g].map(i=>F(Z(i))))).flat();return Ka(t,{lines:T,events:await Promise.all(b.map(ht)),owner:r,working:await $t(o),fromAsk:m,links:l?await pr(m):[],agents:a,moved:y})}u(Ce,"thread");async function He(t){for(const e of Li){let a=w.resolve(e,"."+t);if(a!==e&&!a.startsWith(e+w.sep))return null;if((await k.stat(a).catch(()=>null))?.isDirectory()&&(a=w.join(a,"index.html")),await k.stat(a).then(s=>s.isFile(),()=>!1))return a}return null}u(He,"staticFile");async function mr(t){const e=/^\/prototype-([a-z0-9-]+)\.html$/.exec(t)?.[1];return e&&(await Kn(o)).find(a=>a.topic===e)||null}u(mr,"recordedPrototype");async function gr(t,e){try{const a=decodeURIComponent(String(t.headers["x-filename"]||""));return h(e,200,await di(o,t,{name:a,type:t.headers["content-type"],length:Number(t.headers["content-length"])||0}))}catch(a){if(!(a instanceof A)&&!(a instanceof URIError))throw a;return t.resume(),h(e,a.status||400,{error:a.message})}}u(gr,"upload");const yr=/^(text\/html|image\/svg\+xml|application\/(xhtml\+)?xml|text\/xml)$/;async function kr(t,e,a){const n=await ai(o,a);if(!n){e.writeHead(404).end();return}if(e.writeHead(200,{"content-type":n.type.startsWith("text/")?`${n.type}; charset=utf-8`:n.type,"content-length":n.size,"content-disposition":`inline; filename*=UTF-8''${encodeURIComponent(n.name)}`,"cache-control":"private, max-age=86400","x-content-type-options":"nosniff",...yr.test(n.type)?{"content-security-policy":"sandbox"}:{}}),t.method==="HEAD"){e.end();return}tn(n.path).pipe(e)}u(kr,"sendUpload");const Ct=new Set;function br(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 ra=Object.defineProperty;var l=(t,e)=>ra(t,"name",{value:e,configurable:!0});async function re(t,e){let a;try{a=await k.open(p.join(t,"data","try",`${e}.log`),"r");const{size:n}=await a.stat(),i=Math.max(0,n-64*1024),r=Buffer.alloc(n-i),{bytesRead:d}=await a.read(r,0,r.length,i),u=r.subarray(0,d).toString("utf8").split(/\r?\n/);return i&&u.shift(),u.at(-1)===""&&u.pop(),u.slice(-40)}catch(n){if(n.code==="ENOENT")return[];throw n}finally{await a?.close()}}l(re,"tryLogTail");import{topicItems as sa}from"./review.mjs";import{boardWorktree as ht,builderRepo as oa,helperWorktree as ca,moveQueuedLand as la,prWorktree as ua}from"./worktree.mjs";import{transcriptMovedAt as da,transcriptCommandRunning as fa}from"./transcript.mjs";import{fromHost as ha}from"./on-host.mjs";import{readSync as wa,writeSync as pa,stamp as ma,patchSince as ga}from"./sync.mjs";import{archOf as ya,readReviews as ka,repoName as ba,startReview as va}from"./arch-review.mjs";import Sa from"node:http";import k from"node:fs/promises";import bt from"node:os";import p from"node:path";import{execFile as se,spawn as Oa}from"node:child_process";import{fileURLToPath as ja}from"node:url";import oe from"node:vm";import $a from"node:zlib";import Pa from"node:crypto";import{promisify as Aa}from"node:util";import{archivedOf as ce,buildBoard as vt,movesOf as Ta,visualsDue as Ea,withHub as Ia,DONE_KEPT as le,ORCHESTRATOR_PUSH_AFTER as Ra,orchestratorWaits as xa,waitsStep as Na,PROTOTYPE_WAIT as _a,prototypeWaits as Da,idleStep as Ca,idleSummary as Ha,intakeDue as La,intakeHung as Ba,intakeStranded as ue,INTAKE_TIMEOUT_AFTER as Fa,ITEM_EVENTS as Ma,QUESTION_REOPEN as St,lineupMoved as Ua,slimDone as de,needsAnswer as Ja,ownerActions as Wa,picksNoTake as Ga,pullOf as Ka,arrivalsOf as za,rebasesDue as Va,refOf as Ot,roundsDue as Qa,strictlyIdle as Ya,threadOf as Xa,triageOf as Za}from"./domain.mjs";import{localDay as qa,quietMiddleOf as tn,quietOf as en}from"../hub/schedule.mjs";import{cardPRs as an,prKeyOf as fe}from"../shared/card-prs.mjs";import{dropQueued as nn}from"../hub/push.mjs";import{closeSync as rn,createReadStream as sn,existsSync as he,mkdirSync as on,openSync as cn,readdirSync as ln,statSync as we,watch as pe}from"node:fs";import{PROTOTYPE_TEMPLATE as un,esc as dn,prototypePage as fn,versionOf as jt}from"./prototype-page.mjs";import{BOARD_CLI as hn,launchBuilder as K,launchIntake as wn,pidAlive as $t,reviveBuilders as pn,stopAgents as mn}from"./headless.mjs";import{BoardError as E,intakeHandoff as me,quickAsk as gn,startHelper as yn,startVisual as kn,openAgents as bn,isLanding as vn,implementAsIs as Sn,stageImplement as On,stageReply as jn,planAgain as $n,heldMerge as Pn,opensPRs as An,tapMerge as Tn,addTopic as En,appendThread as N,archiveCards as In,archiveCount as Rn,archiveRank as xn,readArchive as ge,boardConfig as Nn,changeLineup as _n,abandonLaunch as Dn,cutOffBuilds as Cn,abandonPlanner as wt,landable as Hn,relaunchLander as Ln,migrateFollowups as Bn,migrateStatuses as Fn,moveCard as Mn,stageImplementAsks as Un,startPlanner as Jn,stripDeadFields as Wn,pickUp as Gn,startRebase as Kn,startTappedBuild as Pt,sendPush as zn,recordOwnerTap as Vn,takeHoldStopped as Qn,clearStall as Yn,mergeCard as Xn,readPrototypes as Zn,finishHandshake as qn,itemSources as tt,readAskLinks as At,readAskProgress as ti,readBoardRules as ei,readBoardSettings as ai,readHandshake as ni,readHidden as ii,readLineupNotes as ri,readMarks as si,readNotes as oi,readWorking as Tt,readUpload as ci,addReviewers as li,removeReviewers as ui,markRead as ye,resolveNoteAnswers as di,settleAnswers as fi,splitSharedNotes as hi,unrecorded as wi,saveBoardRules as pi,saveBoardSettings as mi,saveBoardShare as gi,saveUpload as yi,setBoardPause as ki,setHidden as ke,settingsOf as C,threadFile as bi,wavesDir as vi}from"./store.mjs";import{changed as Si,readHubRules as Oi}from"../hub/rules.mjs";import{boardAt as Q,listenState as ji,linksOf as $i}from"../hub/boards.mjs";import{pendingFor as Pi,shown as Ai}from"../hub/prompts.mjs";import{routeRecord as J}from"../hub/routes.mjs";import{CHOICES as Ti,FOREVER as be,durationEnd as ve}from"../hub/duration.mjs";import{VIEW_PAGES as Ei,shellHtml as Ii}from"../hub/shell.mjs";import{crossSite as Ri,isJSON as xi}from"../starter/cross-site.mjs";import{shareEntries as Ni}from"../hub/share.mjs";import{readUsageGate as z,launchHeld as V,pauseReasonFor as Et,boardPausedUntil as _i,MACHINE_HELD_LINE as Di}from"../shared/usage-gate.mjs";import{runLimit as Ci}from"../hub/usage.mjs";import{audit as Hi,boardAudit as Li}from"../shared/audit.mjs";import{reviewWaits as Bi}from"../shared/skills.mjs";import{boardRepo as It,mainRefOf as Fi}from"../hub/board-repo.mjs";import{listBuildTargets as Mi}from"./build-script.mjs";import{permissionsView as Ui,setPermission as Ji}from"./permissions.mjs";import{SlackError as Se,SLACK_NEEDS_TOKEN as Oe,savedSlack as je,slackStatus as Wi}from"../hub/slack.mjs";import{collaborators as Gi,githubUser as Ki}from"../hub/pr-report.mjs";const et=p.dirname(ja(import.meta.url));async function $e(){const t=await k.stat(p.join(et,"..","hub","icon-180.png")).catch(()=>null);return`/_hub/icon-180.png?v=${t?Math.floor(t.mtimeMs).toString(36):"0"}`}l($e,"touchIcon");async function zi(t){if(!t.includes("<!--header "))return t;const e=oe.createContext({});return oe.runInContext(await k.readFile(p.join(et,"public","ui-page-header.js"),"utf8"),e),t.replace(/<!--header (\{.*?\})-->/g,(a,n)=>e.BoardUI.header(JSON.parse(n)))}l(zi,"pageHeaders");const c=p.resolve(process.argv[2]||process.cwd()),Vi=[p.join(c,"public"),p.join(et,"public"),p.join(et,"..","starter","public"),p.join(et,"..","shared")],v=p.join(c,"data"),A=l(t=>Hi(Li(c),t),"logCall"),H=p.join(v,"events.jsonl"),pt=p.join(v,"replies"),Pe=Number(process.env.PORT||4600),W=(process.env.PREFIX||"").replace(/\/+$/,""),Ae={".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"},Qi=Aa($a.gzip),Yi=/^(text\/|application\/json|image\/svg\+xml)/;async function Te(t,e,a,n){!["GET","HEAD"].includes(t.req?.method)&&!t.req.boardRead&&x++;const i={...a};let r=Buffer.isBuffer(n)?n:Buffer.from(n??"");if(Yi.test(i["content-type"]||"")&&(i.vary="accept-encoding",r.length>=1024&&/\bgzip\b/.test(t.req?.headers["accept-encoding"]||"")))try{r=await Qi(r),i["content-encoding"]="gzip"}catch{}i["content-length"]=r.length,t.writeHead(e,i),t.end(t.req?.method==="HEAD"?void 0:r)}l(Te,"send");const w=l((t,e,a,n={})=>Te(t,e,{"content-type":Ae[".json"],"cache-control":"no-store",...n},JSON.stringify(a)),"json"),Ee=/\b(src|href)="([\w.][\w./-]*\.(?:js|css))"/g;async function Xi(t,e){const a=e.endsWith("/")?e:p.posix.dirname(e),n=[...new Set([...t.matchAll(Ee)].map(r=>r[2]))],i=new Map(await Promise.all(n.map(async r=>{const d=await Fe(p.posix.join(a,r)),u=d?await k.stat(d).catch(()=>null):null;return[r,u?jt(u):null]})));return t.replace(Ee,(r,d,u)=>i.get(u)?`${d}="${u}?v=${i.get(u)}"`:r)}l(Xi,"versionAssets");const Ie=l((t,e)=>String(t||"").split(",").map(a=>a.trim().replace(/^W\//,"")).includes(e.replace(/^W\//,"")),"sameTag"),O=l(async(t,e)=>{try{return JSON.parse(await k.readFile(t,"utf8"))}catch{return e}},"readJSON"),F=l(async t=>{try{return(await k.readFile(t,"utf8")).split(`
3
+ `).filter(Boolean).map(e=>JSON.parse(e))}catch{return[]}},"readLines"),at=l(t=>bi(c,t),"threadFile"),Rt=l(async()=>{const t=await ii(c);return(await F(H)).filter(e=>!t.has(e.id))},"shownEvents");async function L(t,e=64*1024){if(!xi(t.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});let a="";for await(const n of t)if(a+=n,a.length>e)throw Object.assign(new Error("too large"),{status:413});return a?JSON.parse(a):{}}l(L,"readBody");const Zi=new Set(["owner-answer","owner-answers","prototype-request","reask-request",St,"grill-more","visual-request","item-message","ask","ask-edited","ask-withdrawn","ask-bump","ask-done","card-build","card-hold","card-done","card-bump","card-unbump","lineup-move","plan-waves"]),qi="This board is shared with you to look at only.",tr="Merge and Board settings stay the owner\u2019s.";async function er(t,e){const a=t.headers["x-kickflip-shared"];return a?Ni(await B()).find(i=>i.login===a)?.level!=="full"||!["/_claude/events","/api/upload"].includes(e)||t.method!=="POST"?{refused:qi}:{by:{login:a,name:t.headers["tailscale-user-name"]||null}}:null}l(er,"sharedWriter");const M=l(async(t,e)=>(await xt([[t,e]]))[0],"appendEvent");async function xt(t){const e=new Date().toISOString(),a=t.map(([n,i])=>({id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,7),ts:e,kind:n,data:i}));return await k.mkdir(pt,{recursive:!0}),await k.appendFile(H,a.map(n=>JSON.stringify(n)+`
4
+ `).join("")),x++,Je=We(H),a.map(n=>n.id)}l(xt,"appendEvents");async function Re(t){const e=await wi(c,t),a=new Map(((await O(p.join(v,"owner.json"),{items:[]})).items||[]).map(d=>[d.id,d]));if(!e.length)return[];const n=l(d=>a.has(String(d.id))&&Ga(a.get(String(d.id)),d.choice),"again"),i=await xt(e.map(d=>["owner-answer",n(d)?{...d,prototype_again:!0}:d]));await di(c,e);const{prototype:r}=await fi(c,e);x++;for(const d of r)await q(d,"prototype");return i}l(Re,"recordAnswers");async function ar(t){if(!Array.isArray(t)||!t.length)return"Send at least one answer.";const e=new Set(((await O(p.join(v,"owner.json"),{items:[]})).items||[]).map(n=>n.id)),a=t.filter(n=>!e.has(String(n?.id)));return a.length?`${a.map(n=>String(n?.id)).join(", ")} ${a.length===1?"is":"are"} no longer asked here. Reload to see what is, then send again.`:null}l(ar,"answersRefused");async function nr(t){const e=(await I()).owner.find(a=>a.id===t);return e?["open","requested"].includes(e.state)?`${t} has no answer sent, so there is nothing to reopen.`:null:`${t} is no longer asked here. Reload to see what is.`}l(nr,"reopenRefused");async function ir(t){const e=(await I()).owner.find(a=>a.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.`}l(ir,"reaskRefused");async function mt(t){for(const e of["html","json"]){const a=p.join(pt,`${t.id}.${e}`);try{const[n,i]=await Promise.all([k.readFile(a,"utf8"),k.stat(a)]);return{...t,reply:{[e]:e==="json"?JSON.parse(n):n,at:i.mtime.toISOString()}}}catch{}}return{...t,reply:null}}l(mt,"withReply");async function rr(t,e){const a=await Tt(c);let n=await Rt();return t&&(n=n.filter(i=>i.kind===t)),Promise.all(n.reverse().slice(0,e).map(async i=>{const r=await mt(i);return r.reply||!J(a,i.id)?r:{...r,working:J(a,i.id)}}))}l(rr,"listEvents");const Nt=new Map,sr="number,title,url,state,isDraft,reviewDecision,mergeStateStatus,mergeable,statusCheckRollup,mergedAt,headRefName,headRefOid,updatedAt,latestReviews";function or(t,e){return new Promise(a=>{se("gh",["pr","view",String(e),"--repo",t,"--json",sr],{timeout:2e4},(n,i)=>{if(n)return a(null);try{a(JSON.parse(i))}catch{a(null)}})})}l(or,"ghPR");function cr(t,e){return new Promise(a=>{se("gh",["api",`repos/${t}/pulls/${e}/comments?per_page=100`],{timeout:2e4},(n,i)=>{if(n)return a(0);try{const r=JSON.parse(i),d=new Set(r.map(u=>u.in_reply_to_id).filter(Boolean));a(r.filter(u=>!u.in_reply_to_id&&/codex/i.test(u.user?.login||"")&&!d.has(u.id)).length)}catch{a(0)}})})}l(cr,"ghCodexOpen");function lr(t){const e=t||[];let a=0,n=0;for(const i of e){const r=(i.conclusion||i.state||"").toUpperCase(),d=(i.status||"").toUpperCase();["FAILURE","ERROR","CANCELLED","TIMED_OUT","ACTION_REQUIRED","STARTUP_FAILURE"].includes(r)?a++:(d&&d!=="COMPLETED"||r==="PENDING"||r==="EXPECTED")&&n++}return{total:e.length,fail:a,pending:n,state:a?"fail":n?"pending":e.length?"pass":"none"}}l(lr,"checksOf");async function ur(t,e){const a=`${t}#${e}`;let n=Nt.get(a);return n||(n={at:0,value:null,loading:null},Nt.set(a,n)),Date.now()-n.at>45e3&&!n.loading&&(n.loading=Promise.all([or(t,e),cr(t,e)]).then(([i,r])=>{if(i){const{statusCheckRollup:d,latestReviews:u,...h}=i,b=Object.fromEntries((u||[]).map(m=>[m.author?.login,m.state])),y={...h,checks:lr(d),reviews:b,codexOpen:r};JSON.stringify(y)!==JSON.stringify(n.value)&&x++,n.value=y}n.at=Date.now(),n.loading=null})),!n.value&&n.loading&&await n.loading,n.value}l(ur,"prState");const B=l(()=>Nn(c),"config");async function dr(){const t=await k.readdir(pt).catch(()=>[]),e=await Promise.all(t.map(n=>k.stat(p.join(pt,n)).then(i=>i.mtimeMs,()=>0))),a=Math.max(0,...e);return a?new Date(a).toISOString():null}l(dr,"latestReply");async function xe(t){let e=[];try{e=(await k.readdir(t)).filter(i=>i.endsWith(".json"))}catch{}const a=await Promise.all(e.map(i=>O(p.join(t,i),null))),n=Object.create(null);return e.forEach((i,r)=>{if(!a[r])return;const[d,u="main"]=p.basename(i,".json").split("@");(n[d]||=Object.create(null))[u]=a[r]}),n}l(xe,"readAgents");async function _t(){const t=Q(c);return{cfg:await B(),github:t?.github,local:t?.local,hub:t?.hub,remote:t?.remote,branch:t?.branch,topicItems:sa()}}l(_t,"outsideData");async function Ne(){return(await Promise.all((await tt(c)).map(async t=>{const e=await O(t.file,null);return!e||!Array.isArray(e.items)?null:{slug:t.slug,wave:e,agents:await xe(p.join(t.dir,"agents"))}}))).filter(Boolean)}l(Ne,"liveWaves");async function fr(t){const e=await Ne();for(const a of t){const n=e.find(i=>i.slug===a.wave);n?n.wave.items.push(a.item):e.push({slug:a.wave,wave:{items:[a.item]},agents:{}})}return e}l(fr,"withArchived");async function _e({cfg:t,github:e,local:a,hub:n,remote:i,branch:r},d){const u=t.items==="waves";d??=await Ne();const h=Object.create(null);return await Promise.all(d.flatMap(({slug:b,wave:y})=>y.items.map(async m=>{h[`${b}/${m.id}`]=await F(at(`${b}/${m.id}`))}))),{github:e,remote:i,branch:r,reviewers:An({github:e,local:a,hub:n},C(t,{hub:n}))?t.reviewers||[]:[],fromWaves:u,autoMerge:C(t).auto_merge,skipDraft:C(t).skip_draft,autoBuild:C(t).auto_build,tickets:C(t).tickets,waves:d,threads:h,working:await Tt(c),autoReview:C(t).auto_review,incoming:await O(p.join(v,"incoming.json"),null),reviewRecords:await O(p.join(v,"reviews.json"),{})}}l(_e,"itemSnapshot");async function De(t,e=[]){const a=await _e(t,e.length?await fr(e):void 0),{waves:n,threads:i,working:r}=a,d=Object.create(null),u=new Set(e.map(o=>o.key));await Promise.all(n.flatMap(({slug:o,wave:f})=>{const $=a.github||f.repo;return f.items.flatMap(g=>an(g,$).map(async S=>{d[fe(S)]=u.has(`${o}/${g.id}`)?Nt.get(fe(S))?.value??null:await ur(S.repo,S.number)}))}));const h=new Set(["ask","owner-answer","grill-more","visual-request","item-message","wave-triage","overnight-triage"]),b=await Promise.all((await Rt()).map(o=>h.has(o.kind)||J(r,o.id)?mt(o):{...o,reply:null}));await Promise.all(b.filter(o=>o.kind==="ask").map(async o=>{const f=await F(at(`ask:${o.id}`));f.length&&(i[`ask:${o.id}`]=f)}));const y=Object.create(null),m=Object.create(null);for(const{agents:o}of n)for(const f of Object.values(o||{}).flatMap($=>Object.values($))){const $=f.worktree?.path,g=$&&!f.done?da($):null;g!==null&&(y[$]=g),$&&!f.done&&fa($)&&(m[$]=!0)}const j=Object.create(null),P=await O(p.join(v,"headless-builders.json"),{}),s=await O(p.join(v,"landers.json"),{});for(const[o,f]of[...Object.entries(s),...Object.entries(P)])f?.pid===null&&J(P,o)===f&&(j[o]="held"),Number.isInteger(f?.pid)&&($t(f.pid)?j[o]=!0:f.handedBack?j[o]=!1:delete j[o]);return{...a,moved:y,commandRunning:m,alive:j,try:await O(p.join(v,"try.json"),null),main:await O(p.join(v,"main.json"),null),prs:d,events:b,askLinks:await At(c),askProgress:await ti(c),checked:await O(p.join(v,"checked.json"),null),watcher:await O(p.join(v,"watcher.json"),null),replyAt:await dr(),owner:(o=>({...o,items:[...o.items||[],...e.flatMap(f=>f.questions||[])]}))(await O(p.join(v,"owner.json"),{items:[]})),topicItems:t.topicItems,prSlack:await O(p.join(v,"pr-slack.json"),{})}}l(De,"snapshot");async function hr(t){if(!t.length)return[];const e=new Map(vt(await De(await _t(),t)).items.map(a=>[a.key,a]));return t.map(a=>{const n=e.get(a.key);return de({...n,sortAt:a.closed_at,worktree:n.worktree&&!he(n.worktree)?null:n.worktree})})}l(hr,"archivedCards");const Y=W.slice(1)||p.basename(c);let x=0,nt=null;async function I(){const t=await _t(),e=JSON.stringify([x,t]);if(nt?.key!==e){const a=De(t).then(vt);nt={key:e,built:a},a.catch(()=>{nt?.built===a&&(nt=null)})}return nt.built}l(I,"sharedBoard");let Dt=!1;async function Ce(){if(!Dt){Dt=!0;try{const t=await F(H),e=new Set(t.filter(a=>a.kind==="prototype-overdue").map(a=>a.data?.request));for(const a of Da(await I(),t))e.has(a.request)||Date.now()-Date.parse(a.since)<_a||await M("prototype-overdue",a)}catch(t){console.error("Prototype check failed:",t.message)}finally{Dt=!1}}}l(Ce,"checkPrototypeWaits");async function wr(t){const e=await ni(c);if(!e?.ping?.id||e.done_at)return null;const a=t?.find(i=>i.link==="github"),n={events:e.ack?.id===e.ping.id,...a||!t?{github:!!a?.ok}:{}};return n.events&&n.github!==!1?(await qn(c),null):n}l(wr,"setupOf");const He=Ti.map(([t,e])=>({value:t,label:e,ms:t===be?null:ve(new Date(0),t)-new Date(0)}));async function pr(){const t={slack:null,github:[]},e=je();if(!e||Wi().refused)t.slackProblem=Oe;else try{t.slack=await e.people()}catch(n){if(!(n instanceof Se))throw n;t.slackProblem=n.message==="Slack: missing_scope"?"Slack needs the users:read scope; update the app on Hub settings \u203A Slack.":n.message}const a=Q(c)?.github;if(a)try{t.github=await Gi(a)}catch(n){t.githubProblem=`GitHub: ${n.message}`}return t}l(pr,"reviewerPeople");async function mr(t){const e=je();if(!e)throw new E(Oe);return Promise.all(t.map(async a=>{const n=await Ki(a.github.trim());if(!n)throw new E(`GitHub has no user ${a.github.trim()}.`);return{name:a.name.trim(),slack_user:a.slack_user.trim(),dm:await e.openDM(a.slack_user.trim()),github:n}}))}l(mr,"withDMs");async function it(){const t=new Date().toISOString(),{title:e,reviewers:a}=await B(),n=Q(c),i=n?ji(n):"deaf",r=Ia(await I(),{listen:i,prompts:n?Pi(n.route).map(Ai):[]});r.items=r.items.map(u=>u.worktree&&!he(u.worktree)?{...u,worktree:null}:u);const d=n?$i(n,{listen:i}):null;return{generatedAt:t,title:e,reviewers:(a||[]).map(u=>u?.name).filter(u=>typeof u=="string"),reviewerGithub:Object.fromEntries((a||[]).filter(u=>typeof u?.name=="string").map(u=>[u.name,u.github])),...r,links:d,setup:await wr(d),holdChoices:He,arch:n?ya(await ka(c),r.items,r.owner,{repo:ba(It(n.route)),waits:Bi()}):null}}l(it,"board");async function gr(t){["GET","HEAD"].includes(t.method)||x++;const e={...await it(),doneArchived:await Rn(c)};e.owner=e.owner.filter(i=>i.state!=="resolved");const a=await si(c);e.items=e.items.map(i=>({...i.lane==="done"?de(i):i,readAt:a[i.key]||null}));const n=new Set(e.items.map(i=>i.key));return e.ownerTopics=e.ownerTopics.map(({title:i,summary:r,askedAt:d,...u})=>n.has(u.card)?u:{...u,title:i,summary:r,...d?{askedAt:d}:{}}),e}l(gr,"buildPageBoard");const Ct=p.join(v,"sync.json");let X=await wa(Ct),Ht=X.n,Lt=Promise.resolve(),gt=null;function yr(){return Lt=Lt.then(async()=>{const t=X;t.n!==Ht&&(await k.mkdir(v,{recursive:!0}),await pa(Ct,t),Ht=t.n)}),Lt}l(yr,"saveSync");async function kr(t){const e=await gr(t);for((!gt||e.generatedAt>=gt.generatedAt)&&({sync:X,board:gt}=ma(X,e));Ht<X.n;)await yr();return{sync:X,board:gt}}l(kr,"pageBoard");async function U(t,e){if(new URL(t.url,"http://site").searchParams.get("board")==="0")return;const a=await ha(t),{sync:n,board:i}=await kr(t);return t.boardRead=!0,{...(/^\d+$/.test(e)?ga(n,i,Number(e)):null)||i,onHost:a}}l(U,"answerBoard");let Le=Promise.resolve();function br(){const t=Le.then(async()=>{const e=Q(c);if(!e)throw Object.assign(new E("This board isn\u2019t in the hub\u2019s manifest."),{status:404});const{arch:a}=await it();if(a.last?.open)return{key:a.last.key,ref:a.last.ref,existing:!0};const{key:n,data:i}=await va(c,e),r=await M("arch-review",i);return{key:n,id:r,existing:!1}});return Le=t.catch(()=>{}),t}l(br,"runReview");async function vr(t){const[e,a]=t.split("/"),n=(await tt(c)).find(u=>u.slug===e),i=n?await O(n.file,null):null,r=!i?.items?.some(u=>u.id===a)&&(await ge(c)).find(u=>u.key===t),d=Array.isArray(i?.items)&&i.items.find(u=>u.id===a)||r?.item||null;return{item:d,questions:r?.questions||[],agents:d&&n?J(await xe(p.join(n.dir,"agents")),a)||{}:{}}}l(vr,"itemOf");async function Sr(t){const e=J(await At(c),t),{tickets:a}=C(await B()),n=[];for(const i of await tt(c)){const r=await O(i.file,null);for(const d of Array.isArray(r?.items)?r.items:[]){const u=`${i.slug}/${d.id}`;d.from_ask===t?n.unshift({key:u,ref:Ot(r,d,a),became:!0}):u===e&&n.push({key:u,ref:Ot(r,d,a),became:!1})}}return n}l(Sr,"askLinksOf");async function Be(t){const{item:e,agents:a,questions:n}=t.startsWith("ask:")?{item:null,agents:{},questions:[]}:await vr(t),i=await O(p.join(v,"owner.json"),{items:[]}),r={...i,items:[...i.items||[],...n]},d=new Set(r.items.filter(s=>s.topic===`item:${t}`).map(s=>s.id)),u=t.startsWith("ask:"),h=u?t.slice(4):e?.from_ask||null,b=u?[]:Object.entries(await At(c)).filter(([,s])=>s===t).map(([s])=>s),y=[h,...b].filter(Boolean),m=new Set([t,...y.map(s=>`ask:${s}`)]),j=(await Rt()).filter(s=>m.has(s.data?.key)&&Ma.includes(s.kind)||s.kind==="owner-answer"&&d.has(s.data?.id)||s.kind===St&&d.has(s.data?.question)||s.kind==="ask"&&y.includes(s.id)||s.kind==="ask-edited"),P=(await Promise.all([...m].map(s=>F(at(s))))).flat();return Xa(t,{lines:P,events:await Promise.all(j.map(mt)),owner:r,working:await Tt(c),fromAsk:h,links:u?await Sr(h):[],agents:a,moved:b})}l(Be,"thread");async function Fe(t){for(const e of Vi){let a=p.resolve(e,"."+t);if(a!==e&&!a.startsWith(e+p.sep))return null;if((await k.stat(a).catch(()=>null))?.isDirectory()&&(a=p.join(a,"index.html")),await k.stat(a).then(i=>i.isFile(),()=>!1))return a}return null}l(Fe,"staticFile");async function Or(t){const e=/^\/prototype-([a-z0-9-]+)\.html$/.exec(t)?.[1];return e&&(await Zn(c)).find(a=>a.topic===e)||null}l(Or,"recordedPrototype");async function jr(t,e){try{const a=decodeURIComponent(String(t.headers["x-filename"]||""));return w(e,200,await yi(c,t,{name:a,type:t.headers["content-type"],length:Number(t.headers["content-length"])||0}))}catch(a){if(!(a instanceof E)&&!(a instanceof URIError))throw a;return t.resume(),w(e,a.status||400,{error:a.message})}}l(jr,"upload");const $r=/^(text\/html|image\/svg\+xml|application\/(xhtml\+)?xml|text\/xml)$/;async function Pr(t,e,a){const n=await ci(c,a);if(!n){e.writeHead(404).end();return}if(e.writeHead(200,{"content-type":n.type.startsWith("text/")?`${n.type}; charset=utf-8`:n.type,"content-length":n.size,"content-disposition":`inline; filename*=UTF-8''${encodeURIComponent(n.name)}`,"cache-control":"private, max-age=86400","x-content-type-options":"nosniff",...$r.test(n.type)?{"content-security-policy":"sandbox"}:{}}),t.method==="HEAD"){e.end();return}sn(n.path).pipe(e)}l(Pr,"sendUpload");const Bt=new Set;function Ar(t,e){e.writeHead(200,{"content-type":"text/event-stream; charset=utf-8","cache-control":"no-store",connection:"keep-alive"}),e.write(`retry: 3000
5
5
 
6
- `),Ct.add(e),t.on("close",()=>Ct.delete(e))}u(br,"stream");const Be=u(t=>{for(const e of Ct)e.write(t)},"broadcast");setInterval(()=>Be(`: ping
6
+ `),Bt.add(e),t.on("close",()=>Bt.delete(e))}l(Ar,"stream");const Me=l(t=>{for(const e of Bt)e.write(t)},"broadcast");setInterval(()=>Me(`: ping
7
7
 
8
- `),25e3).unref();const vr=100,Sr=500;let pt=null,Fe=0;function Ht({read:t=!1}={}){t||x++;const e=Date.now();pt||(Fe=e),clearTimeout(pt),pt=setTimeout(()=>{pt=null,Be(`event: change
8
+ `),25e3).unref();const Tr=100,Er=500;let yt=null,Ue=0;function Ft({read:t=!1}={}){t||x++;const e=Date.now();yt||(Ue=e),clearTimeout(yt),yt=setTimeout(()=>{yt=null,Me(`event: change
9
9
  data: ${JSON.stringify({at:new Date().toISOString()})}
10
10
 
11
- `),qe(),ta(),ea(),Vt(),Yt(),at(),Zt()},Math.max(0,Math.min(vr,Fe+Sr-e)))}u(Ht,"changed");const Or=new Set(["node_modules","dd","DerivedData"]),Bt=u(t=>String(t).endsWith(".tmp")||String(t).split(/[\\/]/).some(e=>e.startsWith(".")||Or.has(e)),"ignored");let Me=null;const Le=u(t=>{try{const e=ue(t);return`${e.size}:${e.mtimeMs}`}catch{return null}},"statOf");function Ue(t,e){if(!e)return Ht();if(Bt(e))return;const a=w.join(t,String(e));a!==_t&&Ht({read:a===C&&Le(C)===Me})}u(Ue,"heard");const Ft=u(t=>{try{return ue(t).isDirectory()}catch{return!1}},"isFolder");function jr(t){let e=u(()=>{},"stop"),a=!1,n=null;const s=u(()=>{n||(e(),a=!0,n=setTimeout(()=>{n=null,r()},2e3).unref())},"retry"),r=u(()=>{try{e=(process.platform==="linux"?Pr:$r)(t,s)}catch{s();return}a&&Ht()},"start");r()}u(jr,"watchTree");function $r(t,e){const a=de(t,{recursive:!0},(n,s)=>{Ue(t,s),Ft(t)||e()});return a.on("error",e),()=>a.close()}u($r,"watchRecursive");function Pr(t,e){const a=new Map,n=u(r=>{for(const[d,l]of a)(d===r||d.startsWith(r+w.sep))&&(l.close(),a.delete(d))},"unwatch"),s=u(r=>{if(a.has(r))return;const d=de(r,(l,m)=>{if(Ue(r,m),r===t&&!Ft(t))return e();if(!m||Bt(m))return;const y=w.join(r,String(m));if(!Ft(y))n(y);else try{s(y)}catch{n(y)}});a.set(r,d),d.on("error",()=>r===t?e():n(r));for(const l of nn(r,{withFileTypes:!0}))if(!(!l.isDirectory()||Bt(l.name)))try{s(w.join(r,l.name))}catch{n(w.join(r,l.name))}},"watchFolder");try{s(t)}catch(r){throw n(t),r}return()=>n(t)}u(Pr,"watchEachFolder");async function Ar(){const t=(await B()).items==="waves"?[wi()]:(await Q(o)).map(a=>a.dir),e=[...new Set([O,...t].map(a=>w.resolve(a)))];for(const a of e.filter(n=>!e.some(s=>s!==n&&n.startsWith(s+w.sep))))jr(a)}u(Ar,"watchBoard");const Er=ka.createServer(async(t,e)=>{try{if(t.method!=="GET"&&t.method!=="HEAD"&&x++,t.method!=="GET"&&t.method!=="HEAD"&&ji(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 a=new URL(t.url,"http://site");let n=a.pathname;J&&(n===J||n.startsWith(J+"/"))&&(n=n.slice(J.length)||"/");const s=t.method!=="GET"&&t.method!=="HEAD"?await Vi(t,n):null;if(s?.refused)return h(e,403,{error:s.refused});if(n==="/_claude/health")return h(e,200,{ok:!0});if(n==="/api/board"){const i=await L(t,String(a.searchParams.get("since")||"")),c=`W/"${i.version}"`;if(Ae(t.headers["if-none-match"],c)){e.writeHead(304,{etag:c,"cache-control":"no-store",vary:"accept-encoding"}).end();return}return h(e,200,i,{etag:c})}if(n==="/api/reviewer-people")return h(e,200,await cr());if(n==="/api/questions"){const i=await R(),c=new Set(i.ownerTopics.find(f=>f.id===String(a.searchParams.get("topic")??""))?.questions||[]);return h(e,200,{questions:i.owner.filter(f=>c.has(f.id)&&f.state==="resolved")})}if(n==="/api/done"){const i=String(a.searchParams.get("before")||""),c=(await he(o)).filter(f=>!i||An(f)<i);return h(e,200,{cards:await sr(c.slice(0,re)),left:Math.max(0,c.length-re)})}if(n==="/api/links")return h(e,200,{links:(await tt()).links});if(n==="/api/stream"&&t.method==="GET")return br(t,e);if(n==="/_claude/needs")return h(e,200,Ba(await tt()));if(n==="/api/working"){const i=await R(),c=new Map(i.items.filter(f=>f.kind==="work").map(f=>[f.key,f.ref]));return h(e,200,{working:i.workingOn.map(f=>({id:f.id,kind:f.kind,card:c.get(f.key)||null,since:f.since}))})}if(n==="/api/thread")return h(e,200,await Ce(String(a.searchParams.get("key")||"")));if(n==="/api/notes"&&a.searchParams.has("wave"))try{return h(e,200,await qn(o,String(a.searchParams.get("wave")),D(await B()).tickets))}catch(i){if(i instanceof A)return h(e,404,{error:i.message});throw i}if(n==="/api/notes"||n==="/api/plan"){const i=String(a.searchParams.get("item")||"");try{const{wave:c,item:f,markdown:$}=await ei(o,i);return h(e,200,{key:i,ref:bt(c,f,D(await B()).tickets),title:f.title,markdown:$})}catch(c){if(c instanceof A)return h(e,404,{error:c.message});throw c}}if(n==="/_claude/events"&&t.method==="POST"){const i=await H(t,524288),c=String(i.kind||"message").slice(0,64);delete i.kind;const f=String(i.since||"");if(delete i.since,s&&!Gi.has(c))return h(e,403,{error:zi});for(const p of[i,...Array.isArray(i.answers)?i.answers.filter(S=>S&&typeof S=="object"):[]])delete p.shared_by,s&&(p.shared_by=s.by);if(c==="card-hold"&&i.hold&&i.for!==void 0){const p=ge(new Date,i.for);if(!p)return h(e,400,{error:"That hold ends in the past or more than 30 days on. Pick another time."});delete i.for,p!==me&&(i.until=p.toISOString())}if(c==="card-build"){const p=String(i.key||""),S=await Ot(o,p,P=>ut(o,p.split("/")[1],P));return h(e,200,{id:await nt(S),board:await L(t,f)})}if(c==="item-message"){const p=String(i.key||""),S=(await R()).items.find(_=>_.key===p&&_.kind==="work");if(!String(i.text||"").trim()&&!(Array.isArray(i.files)&&i.files.length)&&(S?.ready||S?.lane==="todo")){const _=await gn(o,p,mt=>ut(o,p.split("/")[1],mt),s?s.by.name||s.by.login:void 0);return h(e,200,{id:await nt(_),board:await L(t,f)})}S?.lane==="todo"&&i.intent==="build"?await yn(o,p,i):S?.lane==="todo"&&S.state==="ready"&&await kn(o,p,i),x++}if(c==="lineup-move"){const p=String(i.lineup||""),S=Ha(await R(),p,String(i.key||""),i.by===-1?-1:i.by===1?1:0);if(!S)return h(e,400,{error:`That card can't move that way in the wave "${p}".`});try{await Tn(o,p,{phases:S.map(P=>P.join(","))})}catch(P){if(P instanceof A)return h(e,400,{error:P.message});throw P}return h(e,200,{id:null,board:await L(t,f)})}if(c==="owner-answer")return h(e,200,{id:(await Ee([i]))[0]??null,board:await L(t,f)});if(c==="owner-answers"){const p=await Qi(i.answers);return p?h(e,400,{error:p}):h(e,200,{ids:await Ee(i.answers),board:await L(t,f)})}if(c==="card-merge"&&await vn(o,String(i.key||"")).catch(()=>null)){const p=String(i.key);try{await On(o,p)}catch(S){if(S instanceof A)return h(e,400,{error:S.message});throw S}return Br(p),h(e,200,{id:null,board:await L(t,f)})}if(c==="card-merge"){try{await Gn(o,String(i.key||""),{tapped:!0})}catch(p){if(p instanceof A)return h(e,400,{error:p.message});throw p}return h(e,200,{id:null,board:await L(t,f)})}if(c==="card-hold"||c==="card-done"){const p=String(i.key||"");let S,P;try{P=c==="card-hold"&&!i.hold?[]:await pn(o,p),c==="card-done"&&await mn(o,p)&&(P=[]),S=await Wn(o,c,i)}catch(_){return h(e,_ instanceof A?400:500,{error:`Couldn\u2019t write ${p} to its wave file: ${_.message}`})}if(!S)return h(e,400,{error:`${p} is already in Done, so its wave file stays as it is.`});P.length&&(dn(P),await N(o,p,{text:"Stopped its builder."})),c==="card-hold"&&!i.hold&&await Ve(p)}if(c==="reviewers"){const p=u(I=>I===void 0?[]:Array.isArray(I)?I:null,"list"),S=p(i.add),P=p(i.remove),_=u(I=>typeof I=="string"&&I.trim(),"text");if(!S||!P||!S.length&&!P.length)return h(e,400,{error:"Send the people to add and the names to remove, each as a list."});if(!P.every(_)||!S.every(I=>_(I?.name)&&_(I.slack_user)&&_(I.github)))return h(e,400,{error:"Each person to add needs a name, a Slack member and a GitHub login."});try{P.length&&await ii(o,P.map(I=>I.trim())),S.length&&await ni(o,await lr(S))}catch(I){if(I instanceof A||I instanceof ke)return h(e,400,{error:I.message});throw I}const mt=await Tt([...P.length?[["reviewers-removed",{remove:P}]]:[],...S.length?[["reviewers-added",{add:S.map(I=>I.name)}]]:[]]);return h(e,200,{id:mt.at(-1),board:await L(t,f)})}if(c==="reask-request"){const p=await Xi(String(i.question||""));if(p)return h(e,400,{error:p})}if(c===kt){const p=await Yi(String(i.question||""));if(p)return h(e,400,{error:p})}const $=await M(c,i);if((c==="card-bump"||c==="card-unbump")&&sa(o,String(i.key||""),c==="card-bump"?new Date().toISOString():null),(c==="grill-more"||c==="prototype-request")&&i.key){const p={...i.text?{text:String(i.text)}:{},...Array.isArray(i.files)&&i.files.length?{files:i.files}:{},...i.question?{question:String(i.question),note:i.note?String(i.note):void 0,choice:i.choice??void 0}:{}},S=c==="grill-more"?"grill":"prototype";await bn(o,String(i.key),S),await it(String(i.key),S,p)}return c==="visual-request"&&i.key&&await Ke((await R()).items,await F(C)),c==="ask-withdrawn"&&await pe(o,String(i.id||""),!0),h(e,200,{id:$,board:await L(t,f)})}if(n==="/api/rules"&&t.method==="GET")return h(e,200,{board:await Qn(o),hub:mi()});if(n==="/api/rules"&&t.method==="POST"){const i=await H(t);let c;try{c=await ci(o,i.rules,i.base??[])}catch($){if($ instanceof A)return h(e,400,{error:$.message});throw $}const f=pi(c.changes)?await M("rules",{scope:"board",...c.changes}):null;return h(e,200,{rules:c.rules,id:f})}if(n==="/api/settings"&&t.method==="GET")return h(e,200,{settings:await Yn(o),paused_until:Ei(o),pause_choices:Ne});if(n==="/api/build-targets"&&t.method==="GET"){const i=K(o);if(!i)return h(e,404,{error:"This board isn\u2019t in the hub\u2019s manifest."});const c=At(i.route);return h(e,200,{targets:await Ni(c.repo,_i(c))})}if(n==="/api/pause"&&t.method==="POST"){const i=await H(t);if(!i||typeof i!="object"||Array.isArray(i)||Object.keys(i).join()!=="pause"||typeof i.pause!="string")return h(e,400,{error:"Send only {pause: a duration, a date and time, or off}."});let c;try{c=await fi(o,i.pause)}catch(f){if(f instanceof A)return h(e,400,{error:f.message});throw f}return at(),h(e,200,{paused_until:c})}if(n==="/api/permissions"){const i=K(o);if(!i)return h(e,404,{error:"This board isn\u2019t in the hub\u2019s manifest."});const c=At(i.route).repo,f=t.method==="POST"?await H(t):null;try{return f&&await Ci(c,o,f.id,f.on),h(e,200,await Di(c,o))}catch($){if($ instanceof A)return h(e,400,{error:$.message});throw $}}if(n==="/api/try/main/log"&&t.method==="GET")return h(e,200,{lines:await ee(o,"main")});if(n==="/api/try/prototypes"&&t.method==="GET"){const i=(await j(w.join(o,"data","try.json"),{}))?.prototypes||{};return h(e,200,{state:i.state||null,https:i.state==="ready"&&i.https||null,lines:i.state==="failed"?await ee(o,"prototypes"):[]})}if(n==="/api/log"&&t.method==="GET"){const i=a.searchParams.get("file")||"";if(!/^[\w.-]+\.log$/.test(i)||i.startsWith("."))return h(e,400,{error:"file takes a log file\u2019s name."});let c;try{c=await k.open(w.join(gt.homedir(),".claude","kickflip","logs",i),"r");const{size:f}=await c.stat(),$=Math.max(0,f-256*1024),p=Buffer.alloc(f-$),{bytesRead:S}=await c.read(p,0,p.length,$),P=p.subarray(0,S).toString("utf8");return h(e,200,{text:$?P.slice(P.indexOf(`
12
- `)+1):P,truncated:$>0,size:f})}catch(f){if(f.code==="ENOENT")return h(e,404,{error:"No such log."});throw f}finally{await c?.close()}}if(n==="/api/try/main/pull"&&t.method==="POST"){const i=await H(t);if(!i||typeof i!="object"||Array.isArray(i)||Object.keys(i).length)return h(e,400,{error:"Send an empty object."});try{const c=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:J.slice(1)||w.basename(o)}),signal:AbortSignal.timeout(3e4)});return h(e,c.status,await c.json())}catch(c){return h(e,502,{error:`Could not pull: ${c.message}`})}}if(n==="/api/try"&&t.method==="POST"){const i=await H(t);if(!i||typeof i!="object"||Array.isArray(i)||Object.keys(i).some(c=>c!=="key")||typeof i.key!="string")return h(e,400,{error:"Send only the card key."});try{const c=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:J.slice(1)||w.basename(o),key:i.key}),signal:AbortSignal.timeout(1e4)});return h(e,c.status,await c.json())}catch(c){return h(e,502,{error:`Could not start the build: ${c.message}`})}}if(n==="/api/share"&&t.method==="POST")try{return h(e,200,await ui(o,await H(t)))}catch(i){if(i instanceof A)return h(e,400,{error:i.message});throw i}if(n==="/api/settings"&&t.method==="POST"){const i=await H(t);let c;try{c=await li(o,i)}catch(p){if(p instanceof A)return h(e,400,{error:p.message});throw p}const f=c.changed.filter(p=>p!=="idle_notification"),$=f.length?await M("settings",Object.fromEntries(f.map(p=>[p,c.settings[p]??null]))):null;return c.changed.includes("auto_merge")&&c.settings.auto_merge&&await M("check-github",{reason:"auto-merge"}),h(e,200,{settings:c.settings,id:$})}if(n==="/api/arch-review"&&t.method==="POST"){await H(t);try{return h(e,200,await hr())}catch(i){if(i instanceof A)return h(e,i.status||400,{error:i.message});throw i}}if(n==="/api/hide"&&t.method==="POST"){const i=await H(t);try{await pe(o,String(i.id||""),i.hidden!==!1)}catch(c){if(c instanceof A)return h(e,400,{error:c.message});throw c}return h(e,200,{ok:!0})}if(n==="/api/read"&&t.method==="POST"){const{marks:i}=await H(t);return i&&typeof i=="object"&&!Array.isArray(i)&&Object.values(i).every(f=>typeof f=="string"&&!Number.isNaN(Date.parse(f)))?(await we(o,(await R()).items.map(f=>f.key),i),h(e,200,{ok:!0})):h(e,400,{error:"Send {marks: {card key: ISO time}}."})}if(n==="/_claude/events"&&t.method==="GET")return h(e,200,await Zi(a.searchParams.get("kind"),Math.min(500,Number(a.searchParams.get("limit")||100))));if(n==="/api/upload"&&t.method==="POST")return await gr(t,e);if(t.method!=="GET"&&t.method!=="HEAD"){e.writeHead(405).end();return}if(n.startsWith("/uploads/"))return await kr(t,e,decodeURIComponent(n.slice(9)));if(n==="/plan.html"){e.writeHead(302,{location:`notes.html${a.search}`,"cache-control":"no-store"}).end();return}if(n==="/index.html"){e.writeHead(302,{location:`./${a.search}`,"cache-control":"no-store"}).end();return}const r=Si.has(n.slice(1)),d=r?null:await mr(n),l=r?null:d?rn:n==="/prototype.html"?null:await He(decodeURIComponent(n));if(!r&&!l){e.writeHead(404).end();return}const m=r?".html":w.extname(l).toLowerCase(),y=r?null:await k.stat(l);let v=r?Buffer.from(Oi({touchIcon:await Se()})):d?Buffer.from(await on(o,d)):await k.readFile(l);!r&&m===".html"&&(v=(await Mi(v.toString("utf8"))).replaceAll("{{title}}",sn((await B()).title)).replaceAll("{{touchIcon}}",await Se())),!r&&m===".html"&&(v=Buffer.from(await Ji(v,n)));const g=m===".html"?`W/"${Oa.createHash("sha1").update(v).digest("base64url").slice(0,20)}"`:`W/"${vt(y)}"`,b=m!==".html"&&a.searchParams.get("v")===vt(y),T={"content-type":W(je,m)||"application/octet-stream","cache-control":b?"public, max-age=31536000, immutable":"no-cache",etag:g};if(Ae(t.headers["if-none-match"],g)){e.writeHead(304,{etag:g,"cache-control":T["cache-control"],vary:"accept-encoding"}).end();return}return await $e(e,200,T,v)}catch(a){e.headersSent||(a.status===403?e.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}).end(a.message):a instanceof A?h(e,a.status||400,{error:a.message}):e.writeHead(a.status||(a.code==="ENOENT"?404:400)).end())}});let Mt=!1;async function We(){if(!Mt){Mt=!0;try{const t=Date.now(),e=(await R()).items.filter(n=>n.heldUntil&&n.lane!=="done"&&Date.parse(n.heldUntil)<=t),a=e.length?await F(C):[];for(const{key:n,heldUntil:s}of e)await Mn(o,n,s,{announce:!a.some(r=>r.kind==="card-unblocked"&&r.data?.key===n&&r.data?.until===s)}),await Ve(n),E({call:"pick-up",key:n,result:"resumed",reason:"Its hold ended.",inputs:{heldUntil:s}})}catch(t){console.error("Pick-up check failed:",t.message)}finally{Mt=!1}}}u(We,"checkPickUps");let et=null,Lt=!1,U=!1,Je=!1;function at(){return et?(Lt=!0,et):(et=(async()=>{do{Lt=!1;try{await Tr()}catch(t){console.error("Auto check failed:",t.message)}}while(Lt)})().finally(()=>{et=null}),et)}u(at,"checkAuto");async function Tr(){const t=D(await B()),e=await F(C),{items:a,reviews:n}=await R(),{idle:s,pullable:r,waiting:d,pulls:l,helpers:m}=La(a,{cap:t.max_implementers,pulled:await Ze(Gt)}),y=Y({dir:o});Je&&y.state!=="paused"&&await M("usage-resumed",{window:"manual"}),Je=y.manual==="board",U=Pt(y);const v=s&&!U;if(U||await xr(Ja(a)),U||await Fr(a,Qe(a)),U||await Mr(a),U||await Ke(a,e),U||await _r(m),await Ut(),t.auto_pull&&!U&&await Ir(Ua(a,{cap:t.max_planners}).map(i=>a.find(c=>c.key===i))),await Lr(t,e,a,v))return;const g=u(async i=>{const c=await Ot(o,i,f=>ut(o,i.split("/")[1],f),"hub");return E({call:"pull",key:i,result:Rr(c),reason:"An implementer slot was free and it was the top ready card in Todo.",inputs:{cap:t.max_implementers,pullable:r,waiting:d}}),nt(c)},"startPull");if(await Wr(Gt,t.auto_pull,d,U?[]:l,g))return;const{due:b,repo:T}=n;!v||!b||(E({call:"pr-review",result:"asked",reason:`The board is idle and PR #${b.number} touches its work.`,inputs:{repo:T,pr:b.number,head:b.head,touches:b.touches,asked:!!b.asked}}),await M("pr-review",{pr:b.number,head:b.head,repo:T,title:b.title,author:b.author,url:b.url,touches:b.touches,asked:!!b.asked}))}u(Tr,"runAuto");const Rr=u(t=>t?t.bumped?"bumped":"started":"refused","startResult");async function nt(t){if(x++,!t||t.bumped)return null;const e={kind:t.kind,model:t.model,relaunched:!1};try{const a=Y({dir:o});ye(a,t.kind)?(E({call:"launch-held",key:t.key,result:"held",reason:Ai(a,t.kind),inputs:{kind:t.kind,model:t.model??null,gate:a}}),a.machine.state==="busy"&&await N(o,t.key,{text:Ti}),await Wt(t.key,{...e,pid:null})):await Wt(t.key,{...e,pid:(await G(o,t)).pid})}catch(a){await Rn(o,t.key,`the launch failed (${a.message}). Tap Implement to try again.`)}return null}u(nt,"handOff");async function Ir(t){for(const{key:e,state:a}of t){const n=a==="prototyping"?"prototype":"grill";E({call:"arrival-planner",key:e,result:n,reason:a==="prototyping"?"It landed in Todo filed prototyping.":"It landed in Todo, not filed prototyping.",inputs:{state:a??null}}),await N(o,e,{text:`Landed in Todo, so the board started its ${n}.`}),await it(e,n),x++}}u(Ir,"launchArrivals");async function xr(t){for(const{key:e,kind:a}of t)E({call:"round-planner",key:e,result:a,reason:"It was sent back from staging or ready for another round.",inputs:{kind:a}}),await N(o,e,{text:`Back to ${a==="grill"?"grilling":"prototyping"}, so the board started its ${a}.`}),await it(e,a),x++}u(xr,"launchRounds");async function _r(t){for(const{key:e,name:a}of t){x++;try{const n=await hn(o,e,a,s=>ra(e.split("/")[1],a,s));if(!n)continue;E({call:"helper",key:e,result:"launching",reason:"A helper was asked for and pullOf let it launch.",inputs:{name:a,model:n.model,worktree:n.worktree}});try{await G(o,n)}catch(s){await dt(o,e,`Couldn't launch helper ${a} (${s.message}).`,a)}}catch(n){if(!(n instanceof A))throw n;await dt(o,e,`Couldn't start helper ${a}: ${n.message}`,a)}}}u(_r,"launchHelpers");let Ge=Promise.resolve();function Ke(t,e){const a=Ge.then(()=>Nr(t,e));return Ge=a.catch(()=>{}),a}u(Ke,"launchVisuals");async function Nr(t,e){for(const{key:a,request:n}of Pa(t,e))try{const s=await wn(o,a,n);if(!s)continue;const{text:r,files:d}=n.data,l={...r?{text:String(r)}:{},...Array.isArray(d)&&d.length?{files:d}:{}};try{await G(o,{...s,...l})}catch(m){await dt(o,a,`Couldn't launch the visual agent (${m.message}).`,s.agent)}}catch(s){if(!(s instanceof A))throw s;await N(o,a,{text:`Couldn't start the visual agent: ${s.message}`})}}u(Nr,"drawVisuals");let ze=Promise.resolve();function Ut(){const t=ze.then(Dr).catch(e=>console.error("Intake check failed:",e.message));return ze=t,t}u(Ut,"sortIntake");async function Dr(){const t=w.join(O,"intake-runs.json"),e=await j(t,{}),a=u(async()=>{await k.mkdir(O,{recursive:!0}),await k.writeFile(t+".tmp",JSON.stringify(e)+`
13
- `),await k.rename(t+".tmp",t)},"save"),n=u(()=>new Date().toISOString(),"now"),s=Object.values(e).filter(y=>!y.ended&&!(y.pid&&St(y.pid)));for(const y of s)y.ended=n();s.length&&await a();const r=await F(C);let{items:d}=await R();se(d,r,e).length&&(x++,{items:d}=await R());for(const y of se(d,r,e))E({call:"intake-handoff",key:y.key,result:"handed off",reason:"Its intake triage run ended without sorting it.",inputs:{event:y.event,kind:y.kind,run:e[y.event]}}),await fe(o,y.event,"the intake triage run ended without sorting it");if(Pt(Y({dir:o})))return;const l=Da(d,r,e);if(!l)return;e[l.event]={key:l.key,pid:null,at:n()},await a();const m={call:"intake-run",key:l.key,reason:`Intake event ${l.event} (${l.kind}) waits to be sorted.`,inputs:{event:l.event,kind:l.kind}};try{const y=await Q(o),v=new Map;for(const i of y)for(const c of(await j(i.file,null))?.items||[])c.plan&&v.set(`${i.slug}/${c.id}`,w.join(i.dir,c.plan));const g=d.filter(i=>i.kind==="work"&&i.lane!=="done").map(i=>({ref:i.ref,key:i.key,title:i.title,lane:i.lane,state:i.state,notes:v.get(i.key)??null})),b=l.key.startsWith("ask:")?l.key.slice(4):null,T=await ln(o,{...l,cards:g,waves:y.map(({slug:i,dir:c})=>({slug:i,dir:c})),quick:b!==null&&await fn(o,b),thread:(await Ce(l.key)).map(({who:i,text:c,files:f})=>({who:i,text:c,files:f}))});e[l.event].pid=T.pid,T.exited.then(Ut),E({...m,result:"launched"})}catch(y){E({...m,result:"failed",reason:`${m.reason} The launch failed: ${y.message}`}),e[l.event].ended=n(),await fe(o,l.event,`intake triage couldn't launch (${y.message})`)}await a()}u(Dr,"runIntake");async function it(t,e,a={}){try{const n=await Bn(o,t,e);if(!n)return;try{await G(o,{...n,...a})}catch(s){await dt(o,t,`Couldn't launch the ${e} (${s.message}).`)}}catch(n){if(!(n instanceof A))throw n;await N(o,t,{text:`Couldn't start the ${e}: ${n.message}`})}}u(it,"launchPlanner");async function Ve(t){const e=await Jn(o,t);e&&(E({call:"resume",key:t,result:e.kind,reason:"Its hold stopped this run.",inputs:e}),e.kind==="grill"||e.kind==="prototype"?await it(t,e.kind):e.label==="Builder"&&await nt(await Ot(o,t,a=>ut(o,t.split("/")[1],a),"hub")))}u(Ve,"resumeRun");async function Wt(t,e){const a=w.join(O,"headless-builders.json"),n=await j(a,{});e?n[t]=e:delete n[t],await k.mkdir(O,{recursive:!0}),await k.writeFile(a+".tmp",JSON.stringify(n)+`
14
- `),await k.rename(a+".tmp",a)}u(Wt,"trackLaunch");async function Cr(){try{const t=await j(w.join(O,"headless-builders.json"),{});for(const e of(await In(o)).filter(a=>!t[a.key]))E({call:"launch-cut-off",key:e.key,result:"launching",reason:"A restart cut off the launch after its build started.",inputs:{kind:e.kind,model:e.model??null}}),await nt(e)}catch(t){console.error("Cut-off launch check failed:",t.message)}}u(Cr,"launchCutOffBuilds");const Qe=u(t=>new Set(t.filter(e=>e.builder?.stopped).map(e=>e.key)),"stoppedKeys");let Jt=!1;async function Hr(){if(!Jt){Jt=!0;try{const t=Y({dir:o}),e=w.join(O,"headless-builders.json"),a=await j(e,{});if(!Object.keys(a).length)return;const{items:n}=yt(await Ie(await It())),s=new Set(n.filter(m=>m.lane!=="done"&&m.builder&&!m.builder.planner).map(m=>m.key)),l=await un(a,s,{launch:u(async m=>{const y=a[m.key],v={call:"relaunch",key:m.key,inputs:y,reason:y.pid===null?"Its launch was held by a pause or a busy Mac, which has lifted.":"Its builder exited with its record open, for the first time."};try{const{pid:g}=await G(o,m);return E({...v,result:y.pid===null?"launched":"relaunched"}),{pid:g}}catch(g){return E({...v,result:"failed",reason:`${v.reason} The launch failed: ${g.message}`}),await N(o,m.key,{text:`Couldn't relaunch the builder headless (${g.message}).`}),{pid:null}}},"launch"),handBack:u(async m=>{E({call:"relaunch",key:m.key,result:"handed back",reason:"Its builder exited twice with its record open.",inputs:a[m.key]}),await N(o,m.key,{text:"Its builder exited twice with the build unfinished; it reads stopped until the orchestrator takes it up."})},"handBack"),held:u(m=>ye(t,m),"held"),stopped:Qe(n)});JSON.stringify(l)!==JSON.stringify(a)&&(await k.writeFile(e+".tmp",JSON.stringify(l)+`
15
- `),await k.rename(e+".tmp",e))}catch(t){console.error("Headless builder check failed:",t.message)}finally{Jt=!1}}}u(Hr,"reviveHeadless");function Br(t){const e=w.join(gt.homedir(),".claude","kickflip","logs");en(e,{recursive:!0});const a=an(w.join(e,`merge-${t.split("/")[1]}-${new Date().toISOString().replace(/[:.]/g,"-")}.log`),"a"),n=process.env.KICKFLIP_MERGE_CMD?JSON.parse(process.env.KICKFLIP_MERGE_CMD):[process.execPath,cn],s=ba(n[0],[...n.slice(1),"--site",z,"merge",t],{detached:!0,stdio:["ignore",a,a]});qa(a),s.on("error",r=>N(o,t,{text:`Not merged: the land couldn't start (${r.message}). Tap Merge again.`}).catch(()=>{})),s.unref()}u(Br,"runHeldMerge");async function Fr(t,e){const a=t.some(g=>g.waitsOn==="checking"),n=Object.fromEntries(t.map(g=>[g.key,g.waitsOn==="checking"?"checking":g.lane!=="done"&&g.agents?.some(b=>b.started&&!b.done)?"building":null])),s=w.join(O,"landers.json"),r=Object.fromEntries(Object.entries(await j(s,{})).map(([g,b])=>[g,typeof b=="string"?{verified:b,pid:null}:b])),d=new Set,l=new Set;for(const[g,b]of Object.entries(r))if(!(!b.pid||St(b.pid)||e.has(g))&&n[g]!=="checking"){if(n[g]!=="building"){l.add(g);continue}await _n(o,g),d.add(g),E({call:"lander",key:g,result:"handed back again",reason:"Its lander exited with its builder still open.",inputs:b})}const m=Object.fromEntries(Object.entries(r).filter(([g])=>!d.has(g)&&!l.has(g))),y=u(async()=>{await k.writeFile(s+".tmp",JSON.stringify(m)+`
16
- `),await k.rename(s+".tmp",s)},"save");if(l.size&&await y(),!a&&!d.size)return;const v=(await xn(o)).filter(g=>m[g.key]?.verified!==g.verified);if(v.length){for(const g of v){const b={call:"lander",key:g.key,reason:"Its build was verified and waits to land.",inputs:{verified:g.verified,before:m[g.key]??null}};try{m[g.key]={verified:g.verified,pid:(await G(o,g,{lander:!0})).pid},E({...b,result:"launched"}),await Wt(g.key,null)}catch(T){E({...b,result:"failed",reason:`${b.reason} The launch failed: ${T.message}`}),m[g.key]={verified:g.verified,pid:null},await N(o,g.key,{text:`Couldn't launch the lander headless (${T.message}); land it with board.mjs land ${g.key}.`})}}await y()}}u(Fr,"launchLanders");async function Mr(t){const e=w.join(O,"rebases.json"),a=await j(e,{}),{base:n}=ia(o),s=Wa(t,Object.fromEntries(Object.entries(a).map(([r,d])=>[r,d.conflict])),n);if(s.length)try{for(const r of s){const d=r.key.split("/")[1],l={call:"rebase-run",key:r.key,reason:`Its PR #${r.pr} conflicts with ${n}.`,inputs:{pr:r.pr,conflict:r.conflict,before:a[r.key]??null}};a[r.key]={conflict:r.conflict,pid:null};let m;try{m=await Ln(o,r.key,{pr:r.pr,conflict:r.conflict,base:n},()=>oa(o,d,r.branch))}catch(y){if(!(y instanceof A))throw y;E({...l,result:"refused",reason:`${l.reason} The start refused: ${y.message}`}),await N(o,r.key,{text:`PR #${r.pr} conflicts with ${n}, but the rebase run couldn't start: ${y.message}`});continue}finally{x++}try{a[r.key].pid=(await G(o,m)).pid,E({...l,result:"launched"})}catch(y){E({...l,result:"failed",reason:`${l.reason} The launch failed: ${y.message}`}),await N(o,r.key,{text:`Couldn't launch the rebase run headless (${y.message}); it reads stopped until the orchestrator takes it up.`})}}}finally{await k.writeFile(e+".tmp",JSON.stringify(a)+`
17
- `),await k.rename(e+".tmp",e)}}u(Mr,"launchRebases");async function Ye(){const t=Qa(new Date,Ya(await j(w.join(gt.homedir(),".claude","kickflip","settings.json"),{})));return t&&Va(t)}u(Ye,"triageNight");async function Lr(t,e,a,n){const s=w.join(O,"triaged.json"),r=await j(s,{}),{waiting:d,seen:l}=za(a),m=u(async()=>{await k.mkdir(O,{recursive:!0}),await k.writeFile(s+".tmp",JSON.stringify({seen:l})+`
18
- `),await k.rename(s+".tmp",s)},"save"),y=e.filter(T=>T.kind==="overnight-triage").at(-1);if(y&&!(await ht(y)).reply)return r.seen!==l&&await m(),!0;if(!t.auto_triage||!n||d.length<2||l===r.seen)return!1;const v=await Ye();if(!v||y?.data?.date===v)return!1;const g=`overnight-triage-${v}`;return((await j(w.join(O,"owner.json"),{topics:[]})).topics||[]).some(T=>T.id===g)||await jn(o,{id:g,title:`Overnight triage ${v}`}),E({call:"triage",result:"sent",reason:`Auto triage is on, ${d.length} cards wait on answers, and tonight's triage hadn't gone out.`,inputs:{date:v,cards:d}}),await M("overnight-triage",{date:v,topic:g,cards:d}),await m(),!0}u(Lr,"checkTriage");async function Ur(){if(!D(await B()).auto_triage)return!1;const t=await Ye();return!!t&&(await F(C)).filter(e=>e.kind==="overnight-triage").at(-1)?.data?.date!==t}u(Ur,"triageDue");const Gt={file:"pulled.json",line:"Pulled in: an implementer slot was free."},Xe=u(t=>t.keys||(t.key?[t.key]:[]),"markedOf"),Ze=u(async({file:t})=>Xe(await j(w.join(O,t),{})),"sentOf");async function Wr({file:t,line:e},a,n,s,r){const d=w.join(O,t),l=a?[...n,...s]:[];for(const m of a?s:[])await k.mkdir(w.dirname(Z(m)),{recursive:!0}),await k.appendFile(Z(m),JSON.stringify({ts:new Date().toISOString(),who:"claude",text:e})+`
19
- `),await r(m);return JSON.stringify(Xe(await j(d,{})))!==JSON.stringify(l)&&(await k.mkdir(O,{recursive:!0}),await k.writeFile(d+".tmp",JSON.stringify({keys:l,at:new Date().toISOString()})+`
20
- `),await k.rename(d+".tmp",d)),l.length>0}u(Wr,"sendPulls");async function Kt(t,{key:e,reason:a,inputs:n}){const s={call:"push",key:e,reason:a,inputs:{tag:t.tag,title:t.title,...n}};try{await Un(o,t),E({...s,result:"sent"})}catch(r){if(!(r instanceof A))throw r;const d=r.message.split(`
21
- `).at(-2)||r.message;E({...s,result:"not sent",reason:`${a} Not sent: ${d}`}),console.error(`Push ${t.tag} not sent:`,d)}}u(Kt,"sendOwnPush");let rt=null,zt=!1;function Vt(){return rt?(zt=!0,rt):(rt=(async()=>{do{zt=!1;try{await Jr()}catch(t){console.error("Action push check failed:",t.message)}}while(zt)})().finally(()=>{rt=null}),rt)}u(Vt,"checkActions");async function Jr(){const t=w.join(O,"action-pushes.json"),e=await j(t,null),a=Fa(await R()),n=Object.fromEntries(a.map(s=>[s.tag,{kind:s.kind,ids:s.questions.map(r=>r.id)}]));if(e){for(const r of a){const d=W(e,r.tag),l=r.questions.filter(v=>!d?.ids?.includes(v.id));if(d?.kind===r.kind&&!l.length)continue;const m=l.length?`questions.html#${encodeURIComponent(l[0].id)}`:r.url,y=d?d.kind!==r.kind?`What waits on the owner changed from ${d.kind} to ${r.kind}.`:"A new question waits on the owner.":"Something new waits on the owner.";await Kt({kind:"action",title:r.title,body:l[0]?.question||r.body,url:`/${z}/${m}`,tag:r.tag,now:r.now},{key:r.tag.startsWith("item-")?r.tag.slice(5):void 0,reason:y,inputs:{was:d??null,kind:r.kind,fresh:l.map(v=>v.id)}})}const s=Object.keys(e).filter(r=>!W(n,r));s.length&&await Za(s,z)}JSON.stringify(n)!==JSON.stringify(e)&&(await k.mkdir(O,{recursive:!0}),await k.writeFile(t+".tmp",JSON.stringify(n)+`
22
- `),await k.rename(t+".tmp",t))}u(Jr,"pushActions");let st=null,Qt=!1;function Yt(){return st?(Qt=!0,st):(st=(async()=>{do{Qt=!1;try{await Gr()}catch(t){console.error("Orchestrator wait check failed:",t.message)}}while(Qt)})().finally(()=>{st=null}),st)}u(Yt,"checkWaits");async function Gr(){const t=w.join(O,"orchestrator-waits.json"),e=await j(t,null),a=e?.waits||[],n=Ra(a,Ta(await R()),new Date),s=[];for(const r of n.waits)!r.picked&&!r.pushed&&Date.now()-Date.parse(r.since)>=Ea?(await Kt({kind:"reminder",title:`${r.ref} waits on the orchestrator`,body:`${r.title}: ${r.next}`,url:`/${z}/#item=${encodeURIComponent(r.key)}`,tag:`wait-${r.key}`,now:!0},{key:r.key,reason:`It has waited on the orchestrator since ${r.since} and nobody picked it up.`,inputs:{kind:r.kind,since:r.since}}),s.push({...r,pushed:!0})):s.push(r);(!e||JSON.stringify(s)!==JSON.stringify(a))&&(await k.mkdir(O,{recursive:!0}),await k.writeFile(t+".tmp",JSON.stringify({waits:s})+`
23
- `),await k.rename(t+".tmp",t)),(n.added.length||n.again.length)&&await M("orchestrator-waits",{added:n.added,again:n.again})}u(Gr,"stepWaits");let ot=null,Xt=!1;function Zt(){return ot?(Xt=!0,ot):(ot=(async()=>{do{Xt=!1;try{await Kr()}catch(t){console.error("Idle check failed:",t.message)}}while(Xt)})().finally(()=>{ot=null}),ot)}u(Zt,"checkIdle");async function Kr(){const t=w.join(O,"idle.json"),e=await B();if(!D(e).idle_notification)return k.rm(t,{force:!0});const a=await j(t,{}),n=await R(),s=Ga(n,{pulled:await Ze(Gt)}),{state:r,push:d}=_a(a,{idle:s,now:new Date});d&&await Kt({kind:"reminder",title:`${e.title||z} has gone idle`,body:Na(n.items,a.last),url:`/${z}/`,tag:"idle",now:!0},{reason:"The board has been strictly idle long enough, with idle notifications on.",inputs:{previous:a}}),JSON.stringify(r)!==JSON.stringify(a)&&(await k.mkdir(O,{recursive:!0}),await k.writeFile(t+".tmp",JSON.stringify(r)+`
24
- `),await k.rename(t+".tmp",t))}u(Kr,"stepIdle");let ct=null,qt=!1;function qe(){return ct?(qt=!0,ct):(ct=(async()=>{do{qt=!1;try{const t=$a(await R());for(const e of t)await Cn(o,e.key,e.to,e.why);t.length&&x++}catch(t){console.error("Lane settle failed:",t.message)}}while(qt)})().finally(()=>{ct=null}),ct)}u(qe,"settleLanes");let lt=null,te=!1;function ta(){return lt?(te=!0,lt):(lt=(async()=>{do{te=!1;try{await zr()}catch(t){console.error("Done archive failed:",t.message)}}while(te)})().finally(()=>{lt=null}),lt)}u(ta,"checkArchive");async function ea(){try{await we(o,(await R()).items.map(t=>t.key))}catch(t){console.error("Read marks failed:",t.message)}}u(ea,"markNewCards");async function zr(){if(!ie(await R()).length)return;const t=ie(await tt()),e=t.length?await $n(o,t.map(a=>({key:a.key,closed_at:a.sortAt}))):[];e.length&&console.log(`Done archive: ${e.length} card${e.length===1?"":"s"} moved`)}u(zr,"archiveDone");try{for(const t of await Nn(o))console.log(t)}catch(t){console.error("follow-up migration:",t.message)}try{for(const t of await Dn(o))console.log(t)}catch(t){console.error("status migration:",t.message)}try{for(const t of await Hn(o))console.log(t)}catch(t){console.error("staging migration:",t.message)}try{await Fn(o)}catch(t){console.error("dead-field strip:",t.message)}await Cr(),await qe(),await ta(),await ea(),await Ar(),setInterval(()=>{x++,_e(),Hr(),Ut(),We(),U&&!Pt(Y({dir:o}))?at():Ur().then(t=>{t&&at()},t=>console.error("Triage check failed:",t.message)),Vt(),Yt(),Zt()},6e4).unref(),await _e(),await We(),await at(),Vt(),Yt(),Zt(),Er.listen(Oe,"127.0.0.1",()=>console.log(`site on http://127.0.0.1:${Oe}/ (prefix ${J||"none"})`));
11
+ `),aa(),na(),ia(),Zt(),te(),st(),ae()},Math.max(0,Math.min(Tr,Ue+Er-e)))}l(Ft,"changed");const Ir=new Set(["node_modules","dd","DerivedData"]),Mt=l(t=>String(t).endsWith(".tmp")||String(t).split(/[\\/]/).some(e=>e.startsWith(".")||Ir.has(e)),"ignored");let Je=null;const We=l(t=>{try{const e=we(t);return`${e.size}:${e.mtimeMs}`}catch{return null}},"statOf");function Ge(t,e){if(!e)return Ft();if(Mt(e))return;const a=p.join(t,String(e));a!==Ct&&Ft({read:a===H&&We(H)===Je})}l(Ge,"heard");const Ut=l(t=>{try{return we(t).isDirectory()}catch{return!1}},"isFolder");function Rr(t){let e=l(()=>{},"stop"),a=!1,n=null;const i=l(()=>{n||(e(),a=!0,n=setTimeout(()=>{n=null,r()},2e3).unref())},"retry"),r=l(()=>{try{e=(process.platform==="linux"?Nr:xr)(t,i)}catch{i();return}a&&Ft()},"start");r()}l(Rr,"watchTree");function xr(t,e){const a=pe(t,{recursive:!0},(n,i)=>{Ge(t,i),Ut(t)||e()});return a.on("error",e),()=>a.close()}l(xr,"watchRecursive");function Nr(t,e){const a=new Map,n=l(r=>{for(const[d,u]of a)(d===r||d.startsWith(r+p.sep))&&(u.close(),a.delete(d))},"unwatch"),i=l(r=>{if(a.has(r))return;const d=pe(r,(u,h)=>{if(Ge(r,h),r===t&&!Ut(t))return e();if(!h||Mt(h))return;const b=p.join(r,String(h));if(!Ut(b))n(b);else try{i(b)}catch{n(b)}});a.set(r,d),d.on("error",()=>r===t?e():n(r));for(const u of ln(r,{withFileTypes:!0}))if(!(!u.isDirectory()||Mt(u.name)))try{i(p.join(r,u.name))}catch{n(p.join(r,u.name))}},"watchFolder");try{i(t)}catch(r){throw n(t),r}return()=>n(t)}l(Nr,"watchEachFolder");async function _r(){const t=(await B()).items==="waves"?[vi()]:(await tt(c)).map(a=>a.dir),e=[...new Set([v,...t].map(a=>p.resolve(a)))];for(const a of e.filter(n=>!e.some(i=>i!==n&&n.startsWith(i+p.sep))))Rr(a)}l(_r,"watchBoard");const Dr=Sa.createServer(async(t,e)=>{try{if(t.method!=="GET"&&t.method!=="HEAD"&&x++,t.method!=="GET"&&t.method!=="HEAD"&&Ri(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 a=new URL(t.url,"http://site");let n=a.pathname;W&&(n===W||n.startsWith(W+"/"))&&(n=n.slice(W.length)||"/");const i=t.method!=="GET"&&t.method!=="HEAD"?await er(t,n):null;if(i?.refused)return w(e,403,{error:i.refused});if(n==="/_claude/health")return w(e,200,{ok:!0});if(n==="/api/board"){const s=await U(t,String(a.searchParams.get("since")||"")),o=`W/"${s.version}"`;if(Ie(t.headers["if-none-match"],o)){e.writeHead(304,{etag:o,"cache-control":"no-store",vary:"accept-encoding"}).end();return}return w(e,200,s,{etag:o})}if(n==="/api/reviewer-people")return w(e,200,await pr());if(n==="/api/questions"){const s=await I(),o=new Set(s.ownerTopics.find(f=>f.id===String(a.searchParams.get("topic")??""))?.questions||[]);return w(e,200,{questions:s.owner.filter(f=>o.has(f.id)&&f.state==="resolved")})}if(n==="/api/done"){const s=String(a.searchParams.get("before")||""),o=(await ge(c)).filter(f=>!s||xn(f)<s);return w(e,200,{cards:await hr(o.slice(0,le)),left:Math.max(0,o.length-le)})}if(n==="/api/links")return w(e,200,{links:(await it()).links});if(n==="/api/stream"&&t.method==="GET")return Ar(t,e);if(n==="/_claude/needs")return w(e,200,Ja(await it()));if(n==="/api/working"){const s=await I(),o=new Map(s.items.filter(f=>f.kind==="work").map(f=>[f.key,f.ref]));return w(e,200,{working:s.workingOn.map(f=>({id:f.id,kind:f.kind,card:o.get(f.key)||null,since:f.since}))})}if(n==="/api/thread")return w(e,200,await Be(String(a.searchParams.get("key")||"")));if(n==="/api/notes"&&a.searchParams.has("wave"))try{return w(e,200,await ri(c,String(a.searchParams.get("wave")),C(await B()).tickets))}catch(s){if(s instanceof E)return w(e,404,{error:s.message});throw s}if(n==="/api/notes"||n==="/api/plan"){const s=String(a.searchParams.get("item")||"");try{const{wave:o,item:f,markdown:$}=await oi(c,s);return w(e,200,{key:s,ref:Ot(o,f,C(await B()).tickets),title:f.title,markdown:$})}catch(o){if(o instanceof E)return w(e,404,{error:o.message});throw o}}if(n==="/_claude/events"&&t.method==="POST"){const s=await L(t,524288),o=String(s.kind||"message").slice(0,64);delete s.kind;const f=String(s.since||"");if(delete s.since,i&&!Zi.has(o))return w(e,403,{error:tr});for(const g of[s,...Array.isArray(s.answers)?s.answers.filter(S=>S&&typeof S=="object"):[]])delete g.shared_by,i&&(g.shared_by=i.by);if(o==="card-hold"&&s.hold&&s.for!==void 0){const g=ve(new Date,s.for);if(!g)return w(e,400,{error:"That hold ends in the past or more than 30 days on. Pick another time."});delete s.for,g!==be&&(s.until=g.toISOString())}if(o==="card-build"){const g=String(s.key||""),S=await Pt(c,g,T=>ht(c,g.split("/")[1],T));return w(e,200,{id:await ot(S),board:await U(t,f)})}if(o==="item-message"){const g=String(s.key||""),S=(await I()).items.find(D=>D.key===g&&D.kind==="work"),T=!String(s.text||"").trim()&&!(Array.isArray(s.files)&&s.files.length);if(S?.stalled&&await Yn(c,g),T&&(S?.ready||S?.lane==="todo")){const D=await Sn(c,g,kt=>ht(c,g.split("/")[1],kt),i?i.by.name||i.by.login:void 0);return w(e,200,{id:await ot(D),board:await U(t,f)})}S?.lane==="todo"&&s.intent==="build"?await On(c,g,s):S?.lane==="todo"&&S.state==="ready"&&await jn(c,g,s),x++}if(o==="lineup-move"){const g=String(s.lineup||""),S=Ua(await I(),g,String(s.key||""),s.by===-1?-1:s.by===1?1:0);if(!S)return w(e,400,{error:`That card can't move that way in the wave "${g}".`});try{await _n(c,g,{phases:S.map(T=>T.join(","))})}catch(T){if(T instanceof E)return w(e,400,{error:T.message});throw T}return w(e,200,{id:null,board:await U(t,f)})}if(o==="owner-answer")return w(e,200,{id:(await Re([s]))[0]??null,board:await U(t,f)});if(o==="owner-answers"){const g=await ar(s.answers);return g?w(e,400,{error:g}):w(e,200,{ids:await Re(s.answers),board:await U(t,f)})}if(o==="card-merge"&&await Pn(c,String(s.key||"")).catch(()=>null)){const g=String(s.key);try{await Tn(c,g)}catch(S){if(S instanceof E)return w(e,400,{error:S.message});throw S}return Kr(g),w(e,200,{id:null,board:await U(t,f)})}if(o==="card-merge"){try{await Xn(c,String(s.key||""),{tapped:!0})}catch(g){if(g instanceof E)return w(e,400,{error:g.message});throw g}return w(e,200,{id:null,board:await U(t,f)})}if(o==="card-hold"||o==="card-done"){const g=String(s.key||"");let S,T;try{T=o==="card-hold"&&!s.hold?[]:await bn(c,g),o==="card-done"&&await vn(c,g)&&(T=[]),S=await Vn(c,o,s)}catch(D){return w(e,D instanceof E?400:500,{error:`Couldn\u2019t write ${g} to its wave file: ${D.message}`})}if(!S)return w(e,400,{error:`${g} is already in Done, so its wave file stays as it is.`});T.length&&(mn(T),await N(c,g,{text:"Stopped its builder."})),o==="card-hold"&&!s.hold&&await Xe(g)}if(o==="reviewers"){const g=l(R=>R===void 0?[]:Array.isArray(R)?R:null,"list"),S=g(s.add),T=g(s.remove),D=l(R=>typeof R=="string"&&R.trim(),"text");if(!S||!T||!S.length&&!T.length)return w(e,400,{error:"Send the people to add and the names to remove, each as a list."});if(!T.every(D)||!S.every(R=>D(R?.name)&&D(R.slack_user)&&D(R.github)))return w(e,400,{error:"Each person to add needs a name, a Slack member and a GitHub login."});try{T.length&&await ui(c,T.map(R=>R.trim())),S.length&&await li(c,await mr(S))}catch(R){if(R instanceof E||R instanceof Se)return w(e,400,{error:R.message});throw R}const kt=await xt([...T.length?[["reviewers-removed",{remove:T}]]:[],...S.length?[["reviewers-added",{add:S.map(R=>R.name)}]]:[]]);return w(e,200,{id:kt.at(-1),board:await U(t,f)})}if(o==="reask-request"){const g=await ir(String(s.question||""));if(g)return w(e,400,{error:g})}if(o===St){const g=await nr(String(s.question||""));if(g)return w(e,400,{error:g})}const $=await M(o,s);if((o==="card-bump"||o==="card-unbump")&&la(c,String(s.key||""),o==="card-bump"?new Date().toISOString():null),(o==="grill-more"||o==="prototype-request")&&s.key){const g={...s.text?{text:String(s.text)}:{},...Array.isArray(s.files)&&s.files.length?{files:s.files}:{},...s.question?{question:String(s.question),note:s.note?String(s.note):void 0,choice:s.choice??void 0}:{}},S=o==="grill-more"?"grill":"prototype";await $n(c,String(s.key),S),await q(String(s.key),S,g)}return o==="visual-request"&&s.key&&await Ve((await I()).items,await F(H)),o==="ask-withdrawn"&&await ke(c,String(s.id||""),!0),w(e,200,{id:$,board:await U(t,f)})}if(n==="/api/rules"&&t.method==="GET")return w(e,200,{board:await ei(c),hub:Oi()});if(n==="/api/rules"&&t.method==="POST"){const s=await L(t);let o;try{o=await pi(c,s.rules,s.base??[])}catch($){if($ instanceof E)return w(e,400,{error:$.message});throw $}const f=Si(o.changes)?await M("rules",{scope:"board",...o.changes}):null;return w(e,200,{rules:o.rules,id:f})}if(n==="/api/settings"&&t.method==="GET")return w(e,200,{settings:await ai(c),paused_until:_i(c),pause_choices:He});if(n==="/api/build-targets"&&t.method==="GET"){const s=Q(c);if(!s)return w(e,404,{error:"This board isn\u2019t in the hub\u2019s manifest."});const o=It(s.route);return w(e,200,{targets:await Mi(o.repo,Fi(o))})}if(n==="/api/pause"&&t.method==="POST"){const s=await L(t);if(!s||typeof s!="object"||Array.isArray(s)||Object.keys(s).join()!=="pause"||typeof s.pause!="string")return w(e,400,{error:"Send only {pause: a duration, a date and time, or off}."});let o;try{o=await ki(c,s.pause)}catch(f){if(f instanceof E)return w(e,400,{error:f.message});throw f}return o&&(Gt=!0),st(),w(e,200,{paused_until:o})}if(n==="/api/permissions"){const s=Q(c);if(!s)return w(e,404,{error:"This board isn\u2019t in the hub\u2019s manifest."});const o=It(s.route).repo,f=t.method==="POST"?await L(t):null;try{return f&&await Ji(o,c,f.id,f.on),w(e,200,await Ui(o,c))}catch($){if($ instanceof E)return w(e,400,{error:$.message});throw $}}if(n==="/api/try/main/log"&&t.method==="GET")return w(e,200,{lines:await re(c,"main")});if(n==="/api/try/prototypes"&&t.method==="GET"){const s=(await O(p.join(c,"data","try.json"),{}))?.prototypes||{};return w(e,200,{state:s.state||null,https:s.state==="ready"&&s.https||null,lines:s.state==="failed"?await re(c,"prototypes"):[]})}if(n==="/api/log"&&t.method==="GET"){const s=a.searchParams.get("file")||"";if(!/^[\w.-]+\.log$/.test(s)||s.startsWith("."))return w(e,400,{error:"file takes a log file\u2019s name."});let o;try{o=await k.open(p.join(bt.homedir(),".claude","kickflip","logs",s),"r");const{size:f}=await o.stat(),$=Math.max(0,f-256*1024),g=Buffer.alloc(f-$),{bytesRead:S}=await o.read(g,0,g.length,$),T=g.subarray(0,S).toString("utf8");return w(e,200,{text:$?T.slice(T.indexOf(`
12
+ `)+1):T,truncated:$>0,size:f})}catch(f){if(f.code==="ENOENT")return w(e,404,{error:"No such log."});throw f}finally{await o?.close()}}if(n==="/api/try/main/pull"&&t.method==="POST"){const s=await L(t);if(!s||typeof s!="object"||Array.isArray(s)||Object.keys(s).length)return w(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:W.slice(1)||p.basename(c)}),signal:AbortSignal.timeout(3e4)});return w(e,o.status,await o.json())}catch(o){return w(e,502,{error:`Could not pull: ${o.message}`})}}if(n==="/api/try"&&t.method==="POST"){const s=await L(t);if(!s||typeof s!="object"||Array.isArray(s)||Object.keys(s).some(o=>o!=="key")||typeof s.key!="string")return w(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:W.slice(1)||p.basename(c),key:s.key}),signal:AbortSignal.timeout(1e4)});return w(e,o.status,await o.json())}catch(o){return w(e,502,{error:`Could not start the build: ${o.message}`})}}if(n==="/api/share"&&t.method==="POST")try{return w(e,200,await gi(c,await L(t)))}catch(s){if(s instanceof E)return w(e,400,{error:s.message});throw s}if(n==="/api/settings"&&t.method==="POST"){const s=await L(t);let o;try{o=await mi(c,s)}catch(g){if(g instanceof E)return w(e,400,{error:g.message});throw g}const f=o.changed.filter(g=>g!=="idle_notification"),$=f.length?await M("settings",Object.fromEntries(f.map(g=>[g,o.settings[g]??null]))):null;return o.changed.includes("auto_merge")&&o.settings.auto_merge&&await M("check-github",{reason:"auto-merge"}),w(e,200,{settings:o.settings,id:$})}if(n==="/api/arch-review"&&t.method==="POST"){await L(t);try{return w(e,200,await br())}catch(s){if(s instanceof E)return w(e,s.status||400,{error:s.message});throw s}}if(n==="/api/hide"&&t.method==="POST"){const s=await L(t);try{await ke(c,String(s.id||""),s.hidden!==!1)}catch(o){if(o instanceof E)return w(e,400,{error:o.message});throw o}return w(e,200,{ok:!0})}if(n==="/api/read"&&t.method==="POST"){const{marks:s}=await L(t);return s&&typeof s=="object"&&!Array.isArray(s)&&Object.values(s).every(f=>typeof f=="string"&&!Number.isNaN(Date.parse(f)))?(await ye(c,(await I()).items.map(f=>f.key),s),w(e,200,{ok:!0})):w(e,400,{error:"Send {marks: {card key: ISO time}}."})}if(n==="/_claude/events"&&t.method==="GET")return w(e,200,await rr(a.searchParams.get("kind"),Math.min(500,Number(a.searchParams.get("limit")||100))));if(n==="/api/upload"&&t.method==="POST")return await jr(t,e);if(t.method!=="GET"&&t.method!=="HEAD"){e.writeHead(405).end();return}if(n.startsWith("/uploads/"))return await Pr(t,e,decodeURIComponent(n.slice(9)));if(n==="/plan.html"){e.writeHead(302,{location:`notes.html${a.search}`,"cache-control":"no-store"}).end();return}if(n==="/index.html"){e.writeHead(302,{location:`./${a.search}`,"cache-control":"no-store"}).end();return}const r=Ei.has(n.slice(1)),d=r?null:await Or(n),u=r?null:d?un:n==="/prototype.html"?null:await Fe(decodeURIComponent(n));if(!r&&!u){e.writeHead(404).end();return}const h=r?".html":p.extname(u).toLowerCase(),b=r?null:await k.stat(u);let y=r?Buffer.from(Ii({touchIcon:await $e()})):d?Buffer.from(await fn(c,d)):await k.readFile(u);!r&&h===".html"&&(y=(await zi(y.toString("utf8"))).replaceAll("{{title}}",dn((await B()).title)).replaceAll("{{touchIcon}}",await $e())),!r&&h===".html"&&(y=Buffer.from(await Xi(y,n)));const m=h===".html"?`W/"${Pa.createHash("sha1").update(y).digest("base64url").slice(0,20)}"`:`W/"${jt(b)}"`,j=h!==".html"&&a.searchParams.get("v")===jt(b),P={"content-type":J(Ae,h)||"application/octet-stream","cache-control":j?"public, max-age=31536000, immutable":"no-cache",etag:m};if(Ie(t.headers["if-none-match"],m)){e.writeHead(304,{etag:m,"cache-control":P["cache-control"],vary:"accept-encoding"}).end();return}return await Te(e,200,P,y)}catch(a){e.headersSent||(a.status===403?e.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}).end(a.message):a instanceof E?w(e,a.status||400,{error:a.message}):e.writeHead(a.status||(a.code==="ENOENT"?404:400)).end())}});let Jt=!1;async function Ke(){if(!Jt){Jt=!0;try{const t=Date.now(),e=(await I()).items.filter(n=>n.heldUntil&&n.lane!=="done"&&Date.parse(n.heldUntil)<=t),a=e.length?await F(H):[];for(const{key:n,heldUntil:i}of e)await Gn(c,n,i,{announce:!a.some(r=>r.kind==="card-unblocked"&&r.data?.key===n&&r.data?.until===i)}),await Xe(n),A({call:"pick-up",key:n,result:"resumed",reason:"Its hold ended.",inputs:{heldUntil:i}})}catch(t){console.error("Pick-up check failed:",t.message)}finally{Jt=!1}}}l(Ke,"checkPickUps");let rt=null,Wt=!1,_=!1,Gt=!1;function st(){return rt?(Wt=!0,rt):(rt=(async()=>{do{Wt=!1;try{await Cr()}catch(t){console.error("Auto check failed:",t.message)}}while(Wt)})().finally(()=>{rt=null}),rt)}l(st,"checkAuto");async function Cr(){const t=C(await B()),e=await F(H),{items:a,reviews:n}=await I(),{idle:i,pullable:r,waiting:d,pulls:u,helpers:h}=Ka(a,{cap:t.max_implementers,pulled:await ea(Qt)}),b=z({dir:c});Gt&&b.state!=="paused"&&await M("usage-resumed",{window:"manual"}),Gt=b.manual==="board",_=V(b,"claude");const y=i&&!_;if(_||await Jr(),_||await Br(Qa(a)),_||await zr(a,Ze(a)),_||await Vr(a),_||await Ve(a,e),_||await Fr(h),await Kt(),t.auto_pull&&!_&&await Lr(za(a,{cap:t.max_planners}).map(o=>a.find(f=>f.key===o))),await Qr(t,e,a,y))return;const m=l(o=>V(o,"claude")?[]:o.near&&!o.near.going?u.slice(0,1):u,"pullNow"),j=l(async o=>{const f=await Pt(c,o,$=>ht(c,o.split("/")[1],$),"hub");return A({call:"pull",key:o,result:Hr(f),reason:"An implementer slot was free and it was the top ready card in Todo.",inputs:{cap:t.max_implementers,pullable:r,waiting:d}}),ot(f)},"startPull");if(await Xr(Qt,t.auto_pull,d,_?[]:m(z({dir:c})),j))return;const{due:P,repo:s}=n;!y||!P||(A({call:"pr-review",result:"asked",reason:`The board is idle and PR #${P.number} touches its work.`,inputs:{repo:s,pr:P.number,head:P.head,touches:P.touches,asked:!!P.asked}}),await M("pr-review",{pr:P.number,head:P.head,repo:s,title:P.title,author:P.author,url:P.url,touches:P.touches,asked:!!P.asked}))}l(Cr,"runAuto");function G(t="claude"){const e=V(z({dir:c}),t);return e&&(_=!0),e}l(G,"launchHeldNow");const Hr=l(t=>t?t.bumped?"bumped":"started":"refused","startResult");async function ot(t){if(x++,!t||t.bumped)return null;const e={kind:t.kind,model:t.model,relaunched:!1};try{const a=z({dir:c});if(V(a,t.kind))A({call:"launch-held",key:t.key,result:"held",reason:Et(a,t.kind),inputs:{kind:t.kind,model:t.model??null,gate:a}}),a.machine.state==="busy"&&await N(c,t.key,{text:Di}),await zt(t.key,{...e,pid:null});else{const{pid:n,log:i}=await K(c,t);await zt(t.key,{...e,pid:n,log:i})}}catch(a){await Dn(c,t.key,`the launch failed (${a.message}). Tap Implement to try again.`)}return null}l(ot,"handOff");async function Lr(t){for(const{key:e,state:a}of t){if(G())return;const n=a==="prototyping"?"prototype":"grill";A({call:"arrival-planner",key:e,result:n,reason:a==="prototyping"?"It landed in Todo filed prototyping.":"It landed in Todo, not filed prototyping.",inputs:{state:a??null}}),await N(c,e,{text:`Landed in Todo, so the board started its ${n}.`}),await q(e,n),x++}}l(Lr,"launchArrivals");async function Br(t){for(const{key:e,kind:a}of t){if(G())return;A({call:"round-planner",key:e,result:a,reason:"It was sent back from staging or ready for another round.",inputs:{kind:a}}),await N(c,e,{text:`Back to ${a==="grill"?"grilling":"prototyping"}, so the board started its ${a}.`}),await q(e,a),x++}}l(Br,"launchRounds");async function Fr(t){for(const{key:e,name:a}of t){if(G())return;x++;try{const n=await yn(c,e,a,i=>ca(e.split("/")[1],a,i));if(!n)continue;A({call:"helper",key:e,result:"launching",reason:"A helper was asked for and pullOf let it launch.",inputs:{name:a,model:n.model,worktree:n.worktree}});try{await K(c,n)}catch(i){await wt(c,e,`Couldn't launch helper ${a} (${i.message}).`,a)}}catch(n){if(!(n instanceof E))throw n;await wt(c,e,`Couldn't start helper ${a}: ${n.message}`,a)}}}l(Fr,"launchHelpers");let ze=Promise.resolve();function Ve(t,e){const a=ze.then(()=>Mr(t,e));return ze=a.catch(()=>{}),a}l(Ve,"launchVisuals");async function Mr(t,e){for(const{key:a,request:n}of Ea(t,e)){if(G())return;try{const i=await kn(c,a,n);if(!i)continue;const{text:r,files:d}=n.data,u={...r?{text:String(r)}:{},...Array.isArray(d)&&d.length?{files:d}:{}};try{await K(c,{...i,...u})}catch(h){await wt(c,a,`Couldn't launch the visual agent (${h.message}).`,i.agent)}}catch(i){if(!(i instanceof E))throw i;await N(c,a,{text:`Couldn't start the visual agent: ${i.message}`})}}}l(Mr,"drawVisuals");let Qe=Promise.resolve();function Kt(){const t=Qe.then(Ur).catch(e=>console.error("Intake check failed:",e.message));return Qe=t,t}l(Kt,"sortIntake");async function Ur(){if(process.env.KICKFLIP_INTAKE==="off")return;const t=p.join(v,"intake-runs.json"),e=await O(t,{}),a=l(async()=>{await k.mkdir(v,{recursive:!0}),await k.writeFile(t+".tmp",JSON.stringify(e)+`
13
+ `),await k.rename(t+".tmp",t)},"save"),n=l(()=>new Date().toISOString(),"now"),i=Object.values(e).filter(y=>!y.ended&&!(y.pid&&$t(y.pid)));for(const y of i)y.ended=n();const r=Ba(e,Date.now()).map(y=>e[y]);for(const y of r)if(y.ended=n(),y.timedOut=!0,!!y.pid){try{process.kill(-y.pid,"SIGTERM")}catch{continue}setTimeout(()=>{try{process.kill(-y.pid,"SIGKILL")}catch{}},5e3).unref()}(i.length||r.length)&&await a();const d=await F(H);let{items:u}=await I();ue(u,d,e).length&&(x++,{items:u}=await I());for(const y of ue(u,d,e)){const m=e[y.event].timedOut,j=m?`the intake triage run timed out after ${Fa/6e4} minutes`:"the intake triage run ended without sorting it";A({call:"intake-handoff",key:y.key,result:"handed off",reason:m?"Its intake triage run timed out.":"Its intake triage run ended without sorting it.",inputs:{event:y.event,kind:y.kind,run:e[y.event]}}),await me(c,y.event,j)}if(G())return;const h=La(u,d,e);if(!h)return;e[h.event]={key:h.key,pid:null,at:n()},await a();const b={call:"intake-run",key:h.key,reason:`Intake event ${h.event} (${h.kind}) waits to be sorted.`,inputs:{event:h.event,kind:h.kind}};try{const y=await tt(c),m=new Map;for(const o of y)for(const f of(await O(o.file,null))?.items||[])f.plan&&m.set(`${o.slug}/${f.id}`,p.join(o.dir,f.plan));const j=u.filter(o=>o.kind==="work"&&o.lane!=="done").map(o=>({ref:o.ref,key:o.key,title:o.title,lane:o.lane,state:o.state,notes:m.get(o.key)??null})),P=h.key.startsWith("ask:")?h.key.slice(4):null,s=await wn(c,{...h,cards:j,waves:y.map(({slug:o,dir:f})=>({slug:o,dir:f})),quick:P!==null&&await gn(c,P),thread:(await Be(h.key)).map(({who:o,text:f,files:$})=>({who:o,text:f,files:$}))});e[h.event].pid=s.pid,s.exited.then(Kt),A({...b,result:"launched"})}catch(y){A({...b,result:"failed",reason:`${b.reason} The launch failed: ${y.message}`}),e[h.event].ended=n(),await me(c,h.event,`intake triage couldn't launch (${y.message})`)}await a()}l(Ur,"runIntake");const Z=p.join(v,"queued-planners.json");async function Ye(t){await k.mkdir(v,{recursive:!0}),await k.writeFile(Z+".tmp",JSON.stringify(t)+`
14
+ `),await k.rename(Z+".tmp",Z)}l(Ye,"saveQueuedPlanners");async function q(t,e,a={}){const n=z({dir:c});if(V(n,"claude")){_=!0,await Ye({...await O(Z,{}),[t]:{kind:e,ask:a}}),A({call:"planner-queued",key:t,result:e,reason:Et(n,"claude"),inputs:{kind:e,gate:n}}),await N(c,t,{text:`The ${e} waits and starts when the hold lifts. ${Et(n,"claude")}`});return}try{const i=await Jn(c,t,e);if(!i)return;try{await K(c,{...i,...a})}catch(r){await wt(c,t,`Couldn't launch the ${e} (${r.message}).`)}}catch(i){if(!(i instanceof E))throw i;await N(c,t,{text:`Couldn't start the ${e}: ${i.message}`})}}l(q,"launchPlanner");async function Xe(t){const e=await Qn(c,t);e&&(A({call:"resume",key:t,result:e.kind,reason:"Its hold stopped this run.",inputs:e}),e.kind==="grill"||e.kind==="prototype"?await q(t,e.kind):e.label==="Builder"&&await ot(await Pt(c,t,a=>ht(c,t.split("/")[1],a),"hub")))}l(Xe,"resumeRun");async function Jr(){for(const[t,{kind:e,ask:a}]of Object.entries(await O(Z,{}))){if(G())return;const{[t]:n,...i}=await O(Z,{});await Ye(i),A({call:"planner-queued",key:t,result:"launching",reason:"The hold it waited on has lifted.",inputs:{kind:e}}),await q(t,e,a),x++}}l(Jr,"launchQueuedPlanners");async function zt(t,e){const a=p.join(v,"headless-builders.json"),n=await O(a,{});e?n[t]=e:delete n[t],await k.mkdir(v,{recursive:!0}),await k.writeFile(a+".tmp",JSON.stringify(n)+`
15
+ `),await k.rename(a+".tmp",a)}l(zt,"trackLaunch");async function Wr(){try{const t=await O(p.join(v,"headless-builders.json"),{});for(const e of(await Cn(c)).filter(a=>!t[a.key]))A({call:"launch-cut-off",key:e.key,result:"launching",reason:"A restart cut off the launch after its build started.",inputs:{kind:e.kind,model:e.model??null}}),await ot(e)}catch(t){console.error("Cut-off launch check failed:",t.message)}}l(Wr,"launchCutOffBuilds");const Ze=l(t=>new Set(t.filter(e=>e.builder?.stopped).map(e=>e.key)),"stoppedKeys");let Vt=!1;async function Gr(){if(!Vt){Vt=!0;try{const t=p.join(v,"headless-builders.json"),e=await O(t,{});if(!Object.keys(e).length)return;const{items:a}=vt(await _e(await _t())),n=new Set(a.filter(h=>h.lane!=="done"&&h.builder&&!h.builder.planner).map(h=>h.key)),i=l(async h=>{A({call:"relaunch",key:h.key,result:"handed back",reason:"Its builder exited twice with its record open.",inputs:e[h.key]}),await N(c,h.key,{text:"Its builder exited twice with the build unfinished; it reads stopped until the orchestrator takes it up."})},"handBack"),r=l(async h=>{const b=e[h.key],y={call:"relaunch",key:h.key,inputs:b,reason:b.until?"Its run stopped at the usage limit, which has reset.":b.pid===null?"Its launch was held by a pause, a busy host or a start near the line, which has lifted.":"Its builder exited with its record open, for the first time."};try{const{pid:m,log:j}=await K(c,h);return A({...y,result:b.pid===null?"launched":"relaunched"}),{pid:m,log:j}}catch(m){return A({...y,result:"failed",reason:`${y.reason} The launch failed: ${m.message}`}),await N(c,h.key,{text:`Couldn't relaunch the builder headless (${m.message}).`}),{pid:null}}},"launch"),d=l(async(h,b)=>{A({call:"relaunch",key:h.key,result:"held",reason:`Its run stopped at the usage limit, which resets at ${b}.`,inputs:e[h.key]}),await N(c,h.key,{text:`Its builder stopped at the usage limit; it starts again at the reset, ${b}.`})},"atLimit"),u=await pn(e,n,{launch:r,handBack:i,held:l(h=>V(z({dir:c}),h),"held"),stopped:Ze(a),limitedUntil:l(h=>h.log?Ci(h.log):null,"limitedUntil"),atLimit:d});JSON.stringify(u)!==JSON.stringify(e)&&(await k.writeFile(t+".tmp",JSON.stringify(u)+`
16
+ `),await k.rename(t+".tmp",t))}catch(t){console.error("Headless builder check failed:",t.message)}finally{Vt=!1}}}l(Gr,"reviveHeadless");function Kr(t){const e=p.join(bt.homedir(),".claude","kickflip","logs");on(e,{recursive:!0});const a=cn(p.join(e,`merge-${t.split("/")[1]}-${new Date().toISOString().replace(/[:.]/g,"-")}.log`),"a"),n=process.env.KICKFLIP_MERGE_CMD?JSON.parse(process.env.KICKFLIP_MERGE_CMD):[process.execPath,hn],i=Oa(n[0],[...n.slice(1),"--site",Y,"merge",t],{detached:!0,stdio:["ignore",a,a]});rn(a),i.on("error",r=>N(c,t,{text:`Not merged: the land couldn't start (${r.message}). Tap Merge again.`}).catch(()=>{})),i.unref()}l(Kr,"runHeldMerge");async function zr(t,e){const a=t.some(m=>m.waitsOn==="checking"),n=Object.fromEntries(t.map(m=>[m.key,m.waitsOn==="checking"?"checking":m.lane!=="done"&&m.agents?.some(j=>j.started&&!j.done)?"building":null])),i=p.join(v,"landers.json"),r=Object.fromEntries(Object.entries(await O(i,{})).map(([m,j])=>[m,typeof j=="string"?{verified:j,pid:null}:j])),d=new Set,u=new Set;for(const[m,j]of Object.entries(r))if(!(!j.pid||$t(j.pid)||e.has(m))&&n[m]!=="checking"){if(n[m]!=="building"){u.add(m);continue}await Ln(c,m),d.add(m),A({call:"lander",key:m,result:"handed back again",reason:"Its lander exited with its builder still open.",inputs:j})}const h=Object.fromEntries(Object.entries(r).filter(([m])=>!d.has(m)&&!u.has(m))),b=l(async()=>{await k.writeFile(i+".tmp",JSON.stringify(h)+`
17
+ `),await k.rename(i+".tmp",i)},"save");if(u.size&&await b(),!a&&!d.size)return;const y=(await Hn(c)).filter(m=>h[m.key]?.verified!==m.verified);if(y.length){for(const m of y){if(G(m.kind??"claude"))break;const j={call:"lander",key:m.key,reason:"Its build was verified and waits to land.",inputs:{verified:m.verified,before:h[m.key]??null}};try{h[m.key]={verified:m.verified,pid:(await K(c,m,{lander:!0})).pid},A({...j,result:"launched"}),await zt(m.key,null)}catch(P){A({...j,result:"failed",reason:`${j.reason} The launch failed: ${P.message}`}),h[m.key]={verified:m.verified,pid:null},await N(c,m.key,{text:`Couldn't launch the lander headless (${P.message}); land it with board.mjs land ${m.key}.`})}}await b()}}l(zr,"launchLanders");async function Vr(t){const e=p.join(v,"rebases.json"),a=await O(e,{}),{base:n}=oa(c),i=Va(t,Object.fromEntries(Object.entries(a).map(([r,d])=>[r,d.conflict])),n);if(i.length)try{for(const r of i){if(G())break;const d=r.key.split("/")[1],u={call:"rebase-run",key:r.key,reason:`Its PR #${r.pr} conflicts with ${n}.`,inputs:{pr:r.pr,conflict:r.conflict,before:a[r.key]??null}};a[r.key]={conflict:r.conflict,pid:null};let h;try{h=await Kn(c,r.key,{pr:r.pr,conflict:r.conflict,base:n},()=>ua(c,d,r.branch))}catch(b){if(!(b instanceof E))throw b;A({...u,result:"refused",reason:`${u.reason} The start refused: ${b.message}`}),await N(c,r.key,{text:`PR #${r.pr} conflicts with ${n}, but the rebase run couldn't start: ${b.message}`});continue}finally{x++}try{a[r.key].pid=(await K(c,h)).pid,A({...u,result:"launched"})}catch(b){A({...u,result:"failed",reason:`${u.reason} The launch failed: ${b.message}`}),await N(c,r.key,{text:`Couldn't launch the rebase run headless (${b.message}); it reads stopped until the orchestrator takes it up.`})}}}finally{await k.writeFile(e+".tmp",JSON.stringify(a)+`
18
+ `),await k.rename(e+".tmp",e)}}l(Vr,"launchRebases");async function qe(){const t=tn(new Date,en(await O(p.join(bt.homedir(),".claude","kickflip","settings.json"),{})));return t&&qa(t)}l(qe,"triageNight");async function Qr(t,e,a,n){const i=p.join(v,"triaged.json"),r=await O(i,{}),{waiting:d,seen:u}=Za(a),h=l(async()=>{await k.mkdir(v,{recursive:!0}),await k.writeFile(i+".tmp",JSON.stringify({seen:u})+`
19
+ `),await k.rename(i+".tmp",i)},"save"),b=e.filter(P=>P.kind==="overnight-triage").at(-1);if(b&&!(await mt(b)).reply)return r.seen!==u&&await h(),!0;if(!t.auto_triage||!n||d.length<2||u===r.seen)return!1;const y=await qe();if(!y||b?.data?.date===y)return!1;const m=`overnight-triage-${y}`;return((await O(p.join(v,"owner.json"),{topics:[]})).topics||[]).some(P=>P.id===m)||await En(c,{id:m,title:`Overnight triage ${y}`}),A({call:"triage",result:"sent",reason:`Auto triage is on, ${d.length} cards wait on answers, and tonight's triage hadn't gone out.`,inputs:{date:y,cards:d}}),await M("overnight-triage",{date:y,topic:m,cards:d}),await h(),!0}l(Qr,"checkTriage");async function Yr(){if(!C(await B()).auto_triage)return!1;const t=await qe();return!!t&&(await F(H)).filter(e=>e.kind==="overnight-triage").at(-1)?.data?.date!==t}l(Yr,"triageDue");const Qt={file:"pulled.json",line:"Pulled in: an implementer slot was free."},ta=l(t=>t.keys||(t.key?[t.key]:[]),"markedOf"),ea=l(async({file:t})=>ta(await O(p.join(v,t),{})),"sentOf");async function Xr({file:t,line:e},a,n,i,r){const d=p.join(v,t),u=a?[...n,...i]:[];for(const h of a?i:[])await k.mkdir(p.dirname(at(h)),{recursive:!0}),await k.appendFile(at(h),JSON.stringify({ts:new Date().toISOString(),who:"claude",text:e})+`
20
+ `),await r(h);return JSON.stringify(ta(await O(d,{})))!==JSON.stringify(u)&&(await k.mkdir(v,{recursive:!0}),await k.writeFile(d+".tmp",JSON.stringify({keys:u,at:new Date().toISOString()})+`
21
+ `),await k.rename(d+".tmp",d)),u.length>0}l(Xr,"sendPulls");async function Yt(t,{key:e,reason:a,inputs:n}){const i={call:"push",key:e,reason:a,inputs:{tag:t.tag,title:t.title,...n}};try{await zn(c,t),A({...i,result:"sent"})}catch(r){if(!(r instanceof E))throw r;const d=r.message.split(`
22
+ `).at(-2)||r.message;A({...i,result:"not sent",reason:`${a} Not sent: ${d}`}),console.error(`Push ${t.tag} not sent:`,d)}}l(Yt,"sendOwnPush");let ct=null,Xt=!1;function Zt(){return ct?(Xt=!0,ct):(ct=(async()=>{do{Xt=!1;try{await Zr()}catch(t){console.error("Action push check failed:",t.message)}}while(Xt)})().finally(()=>{ct=null}),ct)}l(Zt,"checkActions");async function Zr(){const t=p.join(v,"action-pushes.json"),e=await O(t,null),a=Wa(await I()),n=Object.fromEntries(a.map(i=>[i.tag,{kind:i.kind,ids:i.questions.map(r=>r.id)}]));if(e){for(const r of a){const d=J(e,r.tag),u=r.questions.filter(y=>!d?.ids?.includes(y.id));if(d?.kind===r.kind&&!u.length)continue;const h=u.length?`questions.html#${encodeURIComponent(u[0].id)}`:r.url,b=d?d.kind!==r.kind?`What waits on the owner changed from ${d.kind} to ${r.kind}.`:"A new question waits on the owner.":"Something new waits on the owner.";await Yt({kind:"action",title:r.title,body:u[0]?.question||r.body,url:`/${Y}/${h}`,tag:r.tag,now:r.now},{key:r.tag.startsWith("item-")?r.tag.slice(5):void 0,reason:b,inputs:{was:d??null,kind:r.kind,fresh:u.map(y=>y.id)}})}const i=Object.keys(e).filter(r=>!J(n,r));i.length&&await nn(i,Y)}JSON.stringify(n)!==JSON.stringify(e)&&(await k.mkdir(v,{recursive:!0}),await k.writeFile(t+".tmp",JSON.stringify(n)+`
23
+ `),await k.rename(t+".tmp",t))}l(Zr,"pushActions");let lt=null,qt=!1;function te(){return lt?(qt=!0,lt):(lt=(async()=>{do{qt=!1;try{await qr()}catch(t){console.error("Orchestrator wait check failed:",t.message)}}while(qt)})().finally(()=>{lt=null}),lt)}l(te,"checkWaits");async function qr(){const t=p.join(v,"orchestrator-waits.json"),e=await O(t,null),a=e?.waits||[],n=Na(a,xa(await I()),new Date),i=[];for(const r of n.waits)!r.picked&&!r.pushed&&Date.now()-Date.parse(r.since)>=Ra?(await Yt({kind:"reminder",title:`${r.ref} waits on the orchestrator`,body:`${r.title}: ${r.next}`,url:`/${Y}/#item=${encodeURIComponent(r.key)}`,tag:`wait-${r.key}`,now:!0},{key:r.key,reason:`It has waited on the orchestrator since ${r.since} and nobody picked it up.`,inputs:{kind:r.kind,since:r.since}}),i.push({...r,pushed:!0})):i.push(r);(!e||JSON.stringify(i)!==JSON.stringify(a))&&(await k.mkdir(v,{recursive:!0}),await k.writeFile(t+".tmp",JSON.stringify({waits:i})+`
24
+ `),await k.rename(t+".tmp",t)),(n.added.length||n.again.length)&&await M("orchestrator-waits",{added:n.added,again:n.again})}l(qr,"stepWaits");let ut=null,ee=!1;function ae(){return ut?(ee=!0,ut):(ut=(async()=>{do{ee=!1;try{await ts()}catch(t){console.error("Idle check failed:",t.message)}}while(ee)})().finally(()=>{ut=null}),ut)}l(ae,"checkIdle");async function ts(){const t=p.join(v,"idle.json"),e=await B();if(!C(e).idle_notification)return k.rm(t,{force:!0});const a=await O(t,{}),n=await I(),i=Ya(n,{pulled:await ea(Qt)}),{state:r,push:d}=Ca(a,{idle:i,now:new Date});d&&await Yt({kind:"reminder",title:`${e.title||Y} has gone idle`,body:Ha(n.items,a.last),url:`/${Y}/`,tag:"idle",now:!0},{reason:"The board has been strictly idle long enough, with idle notifications on.",inputs:{previous:a}}),JSON.stringify(r)!==JSON.stringify(a)&&(await k.mkdir(v,{recursive:!0}),await k.writeFile(t+".tmp",JSON.stringify(r)+`
25
+ `),await k.rename(t+".tmp",t))}l(ts,"stepIdle");let dt=null,ne=!1;function aa(){return dt?(ne=!0,dt):(dt=(async()=>{do{ne=!1;try{const t=Ta(await I());for(const e of t)await Mn(c,e.key,e.to,e.why);t.length&&x++}catch(t){console.error("Lane settle failed:",t.message)}}while(ne)})().finally(()=>{dt=null}),dt)}l(aa,"settleLanes");let ft=null,ie=!1;function na(){return ft?(ie=!0,ft):(ft=(async()=>{do{ie=!1;try{await es()}catch(t){console.error("Done archive failed:",t.message)}}while(ie)})().finally(()=>{ft=null}),ft)}l(na,"checkArchive");async function ia(){try{await ye(c,(await I()).items.map(t=>t.key))}catch(t){console.error("Read marks failed:",t.message)}}l(ia,"markNewCards");async function es(){if(!ce(await I()).length)return;const t=ce(await it()),e=t.length?await In(c,t.map(a=>({key:a.key,closed_at:a.sortAt,prs:a.prs}))):[];e.length&&console.log(`Done archive: ${e.length} card${e.length===1?"":"s"} moved`)}l(es,"archiveDone");try{for(const t of await Bn(c))console.log(t)}catch(t){console.error("follow-up migration:",t.message)}try{for(const t of await Fn(c))console.log(t)}catch(t){console.error("status migration:",t.message)}try{for(const t of await Un(c))console.log(t)}catch(t){console.error("staging migration:",t.message)}try{for(const t of await hi(c))console.log(t)}catch(t){console.error("notes split:",t.message)}try{await Wn(c)}catch(t){console.error("dead-field strip:",t.message)}await Wr(),await aa(),await na(),await ia(),await _r(),setInterval(()=>{x++,Ce(),Gr(),Kt(),Ke(),_&&!V(z({dir:c}),"claude")?st():Yr().then(t=>{t&&st()},t=>console.error("Triage check failed:",t.message)),Zt(),te(),ae()},6e4).unref(),await Ce(),await Ke(),await st(),Zt(),te(),ae(),Dr.listen(Pe,"127.0.0.1",()=>console.log(`site on http://127.0.0.1:${Pe}/ (prefix ${W||"none"})`));
package/board/stop.mjs CHANGED
@@ -1 +1 @@
1
- var v=Object.defineProperty;var e=(n,a)=>v(n,"name",{value:a,configurable:!0});import{spawnSync as S}from"node:child_process";import*as r from"./store.mjs";import{launchedAgents as l,stopRuns as w}from"./headless.mjs";import{clearStoppedLand as h,landLock as N,landQueue as R,moveQueuedLand as A,runningLand as B}from"./worktree.mjs";import{orchestratorDid as T,plans as f,stoppedWords as m}from"./domain.mjs";import{processStarts as q}from"../hub/live.mjs";const d=e(n=>{throw new r.BoardError(n)},"fail"),b=120,$=e((n,a,t)=>[`${n}: ${m(a)}`,...t.map(i=>`stopped ${i.pid} ${i.args.length>b?`${i.args.slice(0,b-1)}\u2026`:i.args}`)],"stoppedLines");async function x(n,a,t,i){await r.markStopped(n,a,t);const s=await w([i]);return await r.appendThread(n,a,{text:m(t)}),s}e(x,"stopped");async function C(n,a,t){const i=await r.openRuns(n,a),s=i.filter(o=>!f(o));return!s.length&&i.length&&d(`${t} has a ${i[0].kind} planner running, not a build; builder stop stops builds, landers and rebase runs`),s.length&&s.every(o=>!o.pid)&&d(`${t}'s builder wasn't launched by the board, so it isn't the board's to stop; stop it in its own session`),l(s)[0]??d(`${t}'s builder isn't running`)}e(C,"launchedBuild");async function L(n,a,t,i){const s=await r.isLanding(n,a)?"land":"build",o=await x(n,a,s,i.pid);return h(t,o.map(c=>c.pid)),$(t,s,o)}e(L,"stopAgent");async function O(n,a){const t=await r.cardName(n,a);return{lines:await L(n,a,t,await C(n,a,t))}}e(O,"stopBuilder");async function P(n,a){const t=await r.cardName(n,a),i=(await r.openRuns(n,a)).filter(o=>!f(o));i.every(o=>!o.pid)&&d(`${t}'s builder runs in a session the board didn't launch, so blocking it can't stop it; finish or stop it in that session with builder done --next, then block it`);const s=l(i)[0];return s?L(n,a,t,s):[]}e(P,"stopForBlock");const D=e(n=>Number(S("ps",["-o","pgid=","-p",String(n)],{encoding:"utf8"}).stdout.trim())||null,"pgidOf");async function W(n,a){const t=await r.cardName(n,a),i=B(t),s=i&&q([i.pid])?.get(i.pid);(!i||!(s<=Date.parse(i.at)+2e3))&&d(`${t} has no land running`),N()?.pid===i.pid&&d(`${t} is fast-forwarding main now; it can't be stopped partway`);const o=D(i.pid),c=await r.openRuns(n,a),g=l(c.filter(p=>!f(p))).find(p=>p.pid===o);let u;return g?u=await x(n,a,"land",g.pid):i.merge&&o===i.pid?(await r.stopHeldMerge(n,a),u=await w([i.pid]),await r.appendThread(n,a,{text:m("land")})):d(`${t}'s land runs in a session the board didn't launch; stop it in that session`),h(t,u.map(p=>p.pid)),{lines:$(t,"land",u)}}e(W,"stopLand");async function _(n,a,t){const i=await r.cardName(n,a),s=await r.bumpCard(n,a,t),o=A(n,a,t?s:null);return t&&o&&await r.appendThread(n,a,{text:T("land","moved first")}),{lines:[t?`${i} bumped${o?": its land moved first":""}`:`${i} unbumped`]}}e(_,"bump");function j(){const n=R();if(!n.length)return{lines:["no land queued"]};const a=n.filter(t=>!t.running);return{lines:[...n.filter(t=>t.running).map(t=>`${t.card} landing now`),...a.map((t,i)=>`${i+1}. ${t.card}${t.first?" (bumped)":""}`)]}}e(j,"queueLines");export{_ as bump,j as queueLines,O as stopBuilder,P as stopForBlock,W as stopLand};
1
+ var v=Object.defineProperty;var e=(n,t)=>v(n,"name",{value:t,configurable:!0});import{spawnSync as N}from"node:child_process";import*as s from"./store.mjs";import{launchedAgents as f,stopRuns as w}from"./headless.mjs";import{clearStoppedLand as g,landLock as R,landQueue as A,moveQueuedLand as B,runningLand as T}from"./worktree.mjs";import{orchestratorDid as C,plans as m,stoppedWords as h}from"./domain.mjs";import{processStarts as q}from"../hub/live.mjs";const d=e(n=>{throw new s.BoardError(n)},"fail"),b=120,$=e(n=>n.map(t=>`stopped ${t.pid} ${t.args.length>b?`${t.args.slice(0,b-1)}\u2026`:t.args}`),"pidLines"),x=e((n,t,a)=>[`${n}: ${h(t)}`,...$(a)],"stoppedLines");async function L(n,t,a,i){await s.markStopped(n,t,a);const r=await w([i]);return await s.appendThread(n,t,{text:h(a)}),r}e(L,"stopped");async function D(n,t,a){const i=await s.openRuns(n,t),r=i.filter(o=>!m(o));return!r.length&&i.length&&d(`${a} has a ${i[0].kind} planner running, not a build; builder stop stops builds, landers and rebase runs`),r.length&&r.every(o=>!o.pid)&&d(`${a}'s builder wasn't launched by the board, so it isn't the board's to stop; stop it in its own session`),f(r)[0]??d(`${a}'s builder isn't running`)}e(D,"launchedBuild");async function S(n,t,a,i){const r=await s.isLanding(n,t)?"land":"build",o=await L(n,t,r,i.pid);return g(a,o.map(p=>p.pid)),x(a,r,o)}e(S,"stopAgent");async function P(n,t){const a=await s.cardName(n,t);return{lines:await S(n,t,a,await D(n,t,a))}}e(P,"stopBuilder");async function W(n,t){const a=await s.cardName(n,t),i=(await s.openRuns(n,t)).filter(o=>!m(o));i.every(o=>!o.pid)&&d(`${a}'s builder runs in a session the board didn't launch, so blocking it can't stop it; finish or stop it in that session with builder done --next, then block it`);const r=f(i)[0];return r?S(n,t,a,r):[]}e(W,"stopForBlock");async function _(n,t,a){const i=await s.cardName(n,t),r=await s.markStalled(n,t,a);await s.markStopped(n,t,await s.isLanding(n,t)?"land":"build");const o=f((await s.openRuns(n,t)).filter(c=>!m(c)))[0],p=o?await w([o.pid]):[];return g(i,p.map(c=>c.pid)),await s.appendThread(n,t,{text:`stalled by orchestrator: ${r}`}),{lines:[`${i} stalled: ${r}`,...$(p)]}}e(_,"stallCard");const Q=e(n=>Number(N("ps",["-o","pgid=","-p",String(n)],{encoding:"utf8"}).stdout.trim())||null,"pgidOf");async function j(n,t){const a=await s.cardName(n,t),i=T(a),r=i&&q([i.pid])?.get(i.pid);(!i||!(r<=Date.parse(i.at)+2e3))&&d(`${a} has no land running`),R()?.pid===i.pid&&d(`${a} is fast-forwarding main now; it can't be stopped partway`);const o=Q(i.pid),p=await s.openRuns(n,t),c=f(p.filter(u=>!m(u))).find(u=>u.pid===o);let l;return c?l=await L(n,t,"land",c.pid):i.merge&&o===i.pid?(await s.stopHeldMerge(n,t),l=await w([i.pid]),await s.appendThread(n,t,{text:h("land")})):d(`${a}'s land runs in a session the board didn't launch; stop it in that session`),g(a,l.map(u=>u.pid)),{lines:x(a,"land",l)}}e(j,"stopLand");async function z(n,t,a){const i=await s.cardName(n,t),r=await s.bumpCard(n,t,a),o=B(n,t,a?r:null);return a&&o&&await s.appendThread(n,t,{text:C("land","moved first")}),{lines:[a?`${i} bumped${o?": its land moved first":""}`:`${i} unbumped`]}}e(z,"bump");function I(){const n=A();if(!n.length)return{lines:["no land queued"]};const t=n.filter(a=>!a.running);return{lines:[...n.filter(a=>a.running).map(a=>`${a.card} landing now`),...t.map((a,i)=>`${i+1}. ${a.card}${a.first?" (bumped)":""}`)]}}e(I,"queueLines");export{z as bump,I as queueLines,_ as stallCard,P as stopBuilder,W as stopForBlock,j as stopLand};