@lovek4d/kickflip 0.1.0 → 0.3.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 (96) hide show
  1. package/CONTEXT.md +177 -78
  2. package/README.md +46 -5
  3. package/SKILL.md +58 -37
  4. package/bin/doctor.mjs +4 -3
  5. package/bin/install.mjs +3 -3
  6. package/bin/kickflip.mjs +87 -32
  7. package/bin/statusline.mjs +4 -3
  8. package/bin/wait-events.mjs +2 -2
  9. package/board/arch-review.mjs +5 -0
  10. package/board/bin/agent.mjs +1 -1
  11. package/board/bin/board.mjs +241 -69
  12. package/board/bin/builder-stop-hook.mjs +4 -3
  13. package/board/domain.mjs +5 -3
  14. package/board/on-host.mjs +1 -0
  15. package/board/public/app-arch.js +75 -0
  16. package/board/public/app-asks.js +33 -14
  17. package/board/public/app-cards.js +242 -49
  18. package/board/public/app-core.js +39 -5
  19. package/board/public/app-header.js +24 -6
  20. package/board/public/app-lanes.js +76 -26
  21. package/board/public/app-needs.js +1 -0
  22. package/board/public/app-snapshot.js +45 -11
  23. package/board/public/app-threads.js +13 -11
  24. package/board/public/app.html +4 -1
  25. package/board/public/app.js +131 -20
  26. package/board/public/dialogs.js +114 -116
  27. package/board/public/hub-kit.js +19 -2
  28. package/board/public/hub.css +24 -18
  29. package/board/public/linear-key.js +141 -0
  30. package/board/public/router.js +6 -3
  31. package/board/public/rules-editor.js +1 -1
  32. package/board/public/shell.js +1 -1
  33. package/board/public/sidebar.css +31 -11
  34. package/board/public/sidebar.js +88 -16
  35. package/board/public/style.css +141 -46
  36. package/board/public/toasts.js +6 -4
  37. package/board/public/ui-cards.js +102 -37
  38. package/board/public/ui-header.js +17 -8
  39. package/board/public/ui-hub.js +30 -12
  40. package/board/public/ui-links.js +18 -10
  41. package/board/public/ui-order.js +18 -0
  42. package/board/public/ui-page-header.js +4 -2
  43. package/board/public/ui-questions.js +37 -16
  44. package/board/public/ui-sidebar.js +49 -11
  45. package/board/public/ui-snapshot.js +15 -9
  46. package/board/public/ui-threads.js +25 -16
  47. package/board/public/ui.js +23 -11
  48. package/board/public/view-hub-settings.js +46 -30
  49. package/board/public/view-hub.js +64 -13
  50. package/board/public/view-notes.js +7 -4
  51. package/board/public/view-polling.js +3 -3
  52. package/board/public/view-questions.js +42 -22
  53. package/board/public/view-settings.js +90 -35
  54. package/board/review.mjs +1 -0
  55. package/board/server.mjs +12 -7
  56. package/board/store.mjs +14 -6
  57. package/board/tickets.mjs +3 -3
  58. package/board/worktree.mjs +14 -13
  59. package/docs/hub-session.md +3 -3
  60. package/hooks-plugin/hooks/codex-share-auto.mjs +7 -5
  61. package/hooks-plugin/hooks/watcher-stop.mjs +41 -0
  62. package/hub/awake.mjs +1 -0
  63. package/hub/board-repo.mjs +1 -1
  64. package/hub/boards.mjs +4 -4
  65. package/hub/close-board.mjs +1 -1
  66. package/hub/duration.mjs +1 -0
  67. package/hub/github.mjs +4 -5
  68. package/hub/home.mjs +1 -1
  69. package/hub/hub.mjs +12 -10
  70. package/hub/linear.mjs +2 -2
  71. package/hub/new-board.mjs +2 -2
  72. package/hub/pr-report.mjs +1 -0
  73. package/hub/push.mjs +2 -2
  74. package/hub/readiness.mjs +2 -0
  75. package/hub/repos.mjs +3 -2
  76. package/hub/restart-board.mjs +1 -0
  77. package/hub/routes.mjs +1 -1
  78. package/hub/schedule.mjs +1 -1
  79. package/hub/sessions.mjs +2 -2
  80. package/hub/shell.mjs +2 -1
  81. package/hub/slack.mjs +2 -0
  82. package/hub/stream.mjs +12 -12
  83. package/hub/sw.js +1 -1
  84. package/hub/trust.mjs +2 -0
  85. package/hub/try.mjs +4 -3
  86. package/hub/upgrade.mjs +1 -0
  87. package/hub/usage.mjs +3 -3
  88. package/hub/views.mjs +1 -1
  89. package/package.json +1 -1
  90. package/shared/colors.css +7 -5
  91. package/shared/fake-gh.mjs +54 -0
  92. package/shared/service.mjs +16 -16
  93. package/shared/skills.mjs +2 -0
  94. package/shared/usage-gate.mjs +1 -1
  95. package/starter/public/claude.js +3 -2
  96. package/bin/web-ts.mjs +0 -2
package/board/domain.mjs CHANGED
@@ -1,3 +1,5 @@
1
- var Pe=Object.defineProperty;var d=(t,s)=>Pe(t,"name",{value:s,configurable:!0});import{routeRecord as S}from"../hub/routes.mjs";import Ue from"node:fs";import{ticketBadge as we}from"./tickets.mjs";import he from"node:vm";const ye=he.createContext({});he.runInContext(Ue.readFileSync(new URL("./public/ui-enums.js",import.meta.url),"utf8"),ye);const{BUTTON:L,ACTION_KIND:C,BUILDER_KIND:te}=ye.BoardUI,ve=["item-message","mark-ready","grill-more","build-followup","prototype-request"],Bt=["planned","waiting","building","fixing","checking","held","merged","done","dropped"],We=30*6e4,be={review:0,stale:1,progress:2,todo:3,done:4},se=864e5,Fe=7*se,Qe="older",Ge=d(t=>`wave:${t}`,"waveFold"),Ke="asks",Ve=7*se,Se=d(t=>t?new Date(Number(String(t).split(".")[0])*1e3).toISOString():null,"slackTime"),oe=d((t,s)=>({id:t,url:s?.workspace?`https://linear.app/${s.workspace}/issue/${t}`:null}),"linearRef"),He=d((t,s)=>String(t).replace(/^S(\d+)$/,`${s||"INT"}-$1`),"internalRef"),ae=d((t,s,n)=>t.internal===!0?He(s,n?.prefix):s,"refOf"),Oe=d((t,s,n,l)=>we(t||(s?{source:"internal",prefix:"INT"}:null),n,l)||(s?{id:String(s),url:null,source:null}:null),"ticketOf");function Xe(t,s,n){const l=(t.status||"").toLowerCase();return l==="merged"||l==="done"||l==="dropped"||s?.state==="MERGED"||s?.state==="CLOSED"?"done":n||l==="checking"?"progress":s&&s.state==="OPEN"&&!s.isDraft?s.checks?.state==="fail"?"progress":"review":s?"progress":"todo"}d(Xe,"laneOf");const Ye=d(t=>{const s=(t.status||"").toLowerCase();return s==="dropped"||t.pr?.state==="CLOSED"&&s!=="merged"&&s!=="done"},"droppedCard");function ze(t){if(!t.some(l=>l.kind==="work")||t.some(l=>l.lane!=="done"))return null;const s=t.filter(l=>l.lane==="done"),n=s.filter(Ye).length;return{merged:s.length-n,dropped:n}}d(ze,"finishedOf");function Je(t,s,n){return Object.fromEntries(t.map(l=>{const i=S(s?.reviews,l.github),u=i==="APPROVED"?"approved":i==="CHANGES_REQUESTED"?"changes":Object.hasOwn(n,l.name)?"sent":"unsent";return[l.name,{state:u,since:u==="sent"?S(n,l.name):null}]}))}d(Je,"reviewOf");function Ze(t,s,n,l){return n?.kind===te.install?"builder":l==="checking"?"checking":n?"builder":t==="progress"&&s?.state==="OPEN"&&s.isDraft&&s.checks?.state==="pass"?"ready":t==="review"||t==="stale"?s?.reviewDecision==="APPROVED"?"merge":"reviewers":null}d(Ze,"waitsOnOf");const et=/(^|\/)prototype-[\w-]+\.html/,tt={reviewers:"Waiting for PR reviewers",merge:"Approved, merges once CI and the merge state allow"},$e={waiting:"Approved, Merge shows once CI and the merge state allow",ready:"Approved and green: tap Merge",merging:"Merging"},nt="Fix the failing CI";function st({slug:t,wave:s,it:n,order:l,pr:i,repo:u,builder:m,agents:r,eta:p,thread:y,events:w,working:g,topicItem:v,bridgeItem:B,permalink:q,now:o,bumpedAt:A,holdAsk:E,closedAt:j,mergeAt:Q,autoMerge:U,tickets:x}){const P=`${t}/${n.id}`,f=s.reviewers||[],G=Object.fromEntries([...Array.isArray(n.review_sent)?n.review_sent.map(k=>[k,null]):Object.entries(n.review_sent||{}).map(([k,z])=>[k,Se(z)]),...Object.entries(v?.review_link_ts||{}).map(([k,z])=>[k,Se(z)])]),W=Object.values(G).filter(Boolean).sort()[0]||null,K=[...y.map(k=>k.ts),...w.map(k=>k.ts)].sort().pop()||null,$=[...y.filter(k=>(k.who||"claude")!=="love"&&k.kind!=="status").map(k=>k.ts),...w.map(k=>k.reply?.at)].filter(Boolean).sort(),F=w.filter(k=>k.kind==="grill-more").pop();let I=Xe(n,i,!!m);I==="review"&&W&&o-Date.parse(W)>Ve&&(I="stale");const D=Ze(I,i,m,(n.status||"").toLowerCase()),V=D==="builder"&&i?.state==="OPEN"&&!i.isDraft,T=I==="progress"&&!D&&i?.state==="OPEN"&&!i.isDraft,H=n.status==="held"||!!n.held,N=!H&&(I==="todo"||I==="progress"&&D!=="ready"&&D!=="checking"),J=E&&!!E.data?.hold,Y=E&&J!==H&&String(E.ts)>String(n.held_at||"")?J:null,Z=y.filter(k=>(k.who||"claude")==="claude"&&k.kind!=="status").map(k=>String(k.ts)).sort().pop()||"",ee=D==="merge"&&!!Q&&String(Q)>Z,M=D==="merge"&&!U?$e[ee?"merging":"waiting"]:tt[D];return{key:P,kind:"work",order:l,wave:t,waveTitle:s.title||t,id:n.id,ref:ae(s,n.id,x),title:n.title,status:n.status||null,created:n.created||null,internal:s.internal===!0,repo:s.internal===!0&&!s.repo?null:u,lane:I,waitsOn:D,builder:D==="checking"?null:m,agents:r,eta:p,working:g,deps:n.deps||[],held:H,risky:!!n.risky,bumpable:N,bumpedAt:N&&A||null,holdable:I!=="done",holdPending:Y,hasNotes:!!(n.plan||String(n.notes||"").trim()),finishable:I!=="done",closing:I!=="done"&&!!j&&String(j)>String(n.status_at||""),merging:ee,manualSteps:n.manual_steps||[],fromAsk:n.from_ask||null,from:null,next:V||I==="done"||D==="checking"?null:T?nt:M||n.next||null,branch:n.branch||i?.headRefName||null,ticket:x?.source==="linear"&&!n.ticket&&n.linear?.length?null:Oe(x,n.ticket,n.ticket||(s.internal?ae(s,n.id,x):n.linear?.[0]),{repo:u,key:P}),ticketSource:x?.source||null,linear:(n.linear||[]).map(k=>x?.source==="linear"&&we(x,k,{key:P,repo:u})||oe(k,x)),pr:n.pr&&u?{number:n.pr,url:`https://github.com/${u}/pull/${n.pr}`,...i||{}}:null,reviewers:f.map(k=>k.name),review:Je(f,i,G),inReviewSince:W,slack:q(B?.thread_ts),threadCount:y.length+w.length,lastTs:K,othersAt:$,grilling:!!(F&&!F.reply),updatedAt:i?.mergedAt||i?.updatedAt||n.merged_at||m?.at||K||null,ask:null,fold:null,action:null,install:I==="done"?null:ot(n.install,i)}}d(st,"workItem");function ot(t,s){if(!t?.url)return null;const n=t.sha?String(t.sha).toLowerCase():null,l=s?.headRefOid?String(s.headRefOid).toLowerCase():null;return{url:t.url,label:t.label||null,builtAt:t.built_at||null,sha:n,older:!!(n&&l&&!l.startsWith(n))}}d(ot,"installOf");const at=d(t=>t?.state==="OPEN"&&!t.isDraft&&t.reviewDecision==="APPROVED"&&["pass","none"].includes(t.checks?.state)&&t.mergeStateStatus==="CLEAN","mergeable");function lt(t,s,n){if(t.held||t.lane==="done")return null;const l=`#item=${encodeURIComponent(t.key)}`;if(Ae(t)){const i=s.get(t.questions.first);return et.test(i?.url||"")?{kind:C.pick,url:`questions.html#${i.id}`,label:"Prototypes ready: pick a take"}:{kind:C.questions,url:`questions.html#${t.questions.first}`,label:`Answer ${X(t.questions.open,"question")}`}}return t.waitsOn==="ready"?{kind:C.ready,url:t.pr.url,label:"Tap Ready on GitHub"}:t.kind==="ask"&&t.waitsOn==="owner"?{kind:C.done,url:l,label:t.next||"Tap Done once it is settled"}:t.waitsOn==="merge"&&t.manualSteps.length?{kind:C.steps,url:l,label:"Run the manual steps, then merge"}:t.waitsOn==="merge"&&!n&&!t.merging&&at(t.pr)?{kind:C.merge,url:l,label:"Merge"}:null}d(lt,"actionOf");const Ae=d(t=>!!(t.questions&&t.questions.state!=="resolved"&&t.questions.open>0),"asking"),rt=new Set([C.pick,C.questions]);function it(t){return t.lane==="done"?null:Ae(t)?L.openQuestions:t.action?.kind===C.done?L.markDone:t.kind==="ask"?null:L.grill}d(it,"actionButtonOf");const dt=Object.freeze({[C.pick]:L.openQuestions,[C.questions]:L.openQuestions,[C.ready]:L.github,[C.steps]:L.github,[C.merge]:L.merge,[C.done]:L.markDone});function ut(t,s,n){t.action=lt(t,s,n),t.actionButton=it(t),t.resolvedBy=dt[t.action?.kind]||null,t.action?.kind===C.merge&&(t.next=$e.ready);const l=t.lane==="todo"||t.lane==="progress"&&(t.waitsOn==="ready"||t.waitsOn==="owner");if(!t.action||!l)return;const i=t.lane==="todo"&&rt.has(t.action.kind)?"todo":"review";Object.assign(t,{lane:i,waitsOn:t.waitsOn==="ready"?"ready":"owner",next:t.action.label,bumpable:!1,bumpedAt:null})}d(ut,"placeOwnerWait");const ct=d(t=>t.agents.length>0||!!(t.questions&&t.questions.answered+t.questions.replied+t.questions.resolved>0),"started");function pt({slug:t,wave:s,f:n,order:l,repo:i,bumpedAt:u,closedAt:m,tickets:r}){return{key:`${t}/${n.id}`,kind:"followup",order:l,wave:t,waveTitle:s.title||t,id:n.id,ref:n.id,title:n.title,status:null,repo:s.internal===!0&&!s.repo?null:i,finishable:!n.done,closing:!n.done&&!!m,lane:n.done?"done":"todo",waitsOn:null,builder:null,agents:[],eta:null,working:!1,inReviewSince:null,deps:[],held:!1,risky:!1,bumpable:!n.done,bumpedAt:n.done?null:u||null,manualSteps:[],fromAsk:null,from:n.from||null,next:null,hasNotes:!1,branch:null,ticket:Oe(r,n.ticket,n.ticket||n.id,{repo:i,key:`${t}/${n.id}`}),linear:[oe(n.id,r)],pr:null,reviewers:[],review:{},slack:null,threadCount:0,lastTs:null,othersAt:[],grilling:!1,updatedAt:n.filed||null,ask:null,fold:null,action:null}}d(pt,"followUp");function ft(t,s,n,l,i,u,m){const r=s.find(f=>f.kind==="ask"&&f.eventId===t.id),p=t.data?.files||[],y=(r.text||"").trim().split(`
2
- `)[0]||p.map(f=>f.name).join(", ")||"Ask",w=r.state==="replied"||r.state==="answered",g=i?.state==="progress"&&!(String(u||"")>String(i.at||"")),v=!g&&(!!i||!!u),B=w&&!v&&!g&&m.open>0,q=w&&!v&&!g&&!B&&m.answered>0,o=v||w&&!g&&!B&&!q,A=w&&!l&&!i?.moved&&[...s.filter(f=>f.who==="claude"&&f.kind!=="ask-link"&&f.kind!=="status").map(f=>f.ts),...s.map(f=>f.reply?.at)].filter(Boolean).sort().pop()||null,E=o||g||B||q,j=s.some(f=>f.who==="love"&&f.kind!=="ask"&&(f.state==="waiting"||f.state==="working")),Q=E?"answered":r.state,U=E?null:n||null,x=[...s.filter(f=>f.who!=="love"&&f.kind!=="ask-link").map(f=>f.ts),...s.map(f=>f.reply?.at)].filter(Boolean).sort(),P=[...s.map(f=>f.ts),...x,...g?[i.at]:[]].filter(Boolean).sort().pop();return{key:`ask:${t.id}`,kind:"ask",order:0,wave:null,waveTitle:null,id:t.id,ref:E?"Task":"New",isNew:!E,title:y,status:null,internal:!1,repo:null,lane:g||q?"progress":o?"done":"todo",waitsOn:g&&!j?"owner":null,builder:null,agents:[],eta:null,working:s.some(f=>f.state==="working"),deps:[],held:!1,risky:!1,bumpable:!E,bumpedAt:U,manualSteps:[],fromAsk:null,from:null,next:g&&i.next||null,hasNotes:!1,branch:null,finishable:E&&!o,closing:!1,linear:[],pr:null,reviewers:[],review:{},inReviewSince:null,slack:null,threadCount:s.length,lastTs:P,othersAt:E?x:[],grilling:!1,updatedAt:E?P:t.ts,ask:{eventId:t.id,state:Q,askedAt:t.ts,bumpedAt:U,edited:!!r.edited,canEdit:!E,text:r.text,files:p,answerAt:A,from:r.from||null},fold:null,action:null}}d(ft,"askItem");function Ee(t={}){return Object.entries(t).filter(([,s])=>s&&typeof s=="object").map(([s,n])=>({name:s,kind:n.kind||null,label:n.label||null,started:n.started||null,eta:n.done?null:n.eta||null,done:n.done||null})).sort((s,n)=>String(s.started||"").localeCompare(String(n.started||""))||s.name.localeCompare(n.name))}d(Ee,"agentsOf");function mt(t,s=[],n){const l=t.filter(p=>p.started&&!p.done),i=l.map(p=>String(p.started||"")).sort()[0];if(i===void 0)return null;const u=s.filter(p=>p.kind==="status"&&String(p.ts)>=i).sort((p,y)=>String(p.ts).localeCompare(String(y.ts))).pop(),m=l.some(p=>p.kind===te.install)?te.install:l.at(-1).kind,r=u?.ts||i;return{progress:u?.text||null,at:r,kind:m,silentSince:r,stalled:n-Date.parse(r)>=We}}d(mt,"builderOf");function Be({kind:t,label:s}){const n=t?t[0].toUpperCase()+t.slice(1):"";return n?s?s.toLowerCase().includes(t.toLowerCase())?s:`${n} ${s[0].toLowerCase()}${s.slice(1)}`:n:s||"Builder"}d(Be,"agentName");function kt(t,s){const n=s.filter(m=>m.data?.id===t.id),l=n[n.length-1]||null,i=!!(t.resolved&&t.resolved_at&&l&&String(l.ts)>t.resolved_at),u=t.resolved&&!i?"resolved":l?l.reply?"replied":"answered":"open";return{...t,state:u,answers:n.map(m=>({ts:m.ts,choice:m.data.choice||null,note:m.data.note||"",files:m.data.files||[],reply:m.reply}))}}d(kt,"ownerQuestion");function gt(t,s,n=new Set){const l=new Set(t.map(u=>u.id)),i=[...t];return s.some(u=>!l.has(u.topic))&&i.push({id:"",title:"Other"}),i.map(u=>{const m=s.filter(w=>(l.has(w.topic)?w.topic:"")===u.id),r=d(w=>m.filter(g=>g.state===w).length,"count"),p=m.every(w=>w.state==="resolved")||n.has(u.id)&&!r("open"),y=m.length&&p?"resolved":"open";return{...u,state:y,questions:m.map(w=>w.id),open:r("open"),answered:r("answered"),replied:r("replied"),resolved:r("resolved")}}).filter(u=>u.questions.length)}d(gt,"ownerTopics");const wt={"mark-ready":"Mark ready","grill-more":"Grill","prototype-request":"Asked for prototypes"};function xe(t,{lines:s=[],events:n=[],owner:l={},working:i={},fromAsk:u=null,links:m=[],agents:r={},moved:p=[]}){const y=t.startsWith("ask:")?[t.slice(4)]:[u,...p].filter(Boolean),w=new Set([t,...y.map(o=>`ask:${o}`)]),g=new Map((l.items||[]).filter(o=>o.topic===`item:${t}`).map(o=>[o.id,o])),v=[...Ee(r).flatMap(o=>[...o.started?[{ts:o.started,who:"claude",kind:"agent",text:`${Be(o)} started`,finished:!1,eta:o.done?null:o.eta}]:[],...o.done?[{ts:o.done,who:"claude",kind:"agent",text:`${Be(o)} finished`,finished:!0,eta:null}]:[]]),...s.map(o=>({ts:o.ts,who:o.who||"claude",...o.kind==="status"?{kind:"status"}:{},text:o.text||"",html:o.html||null,link:o.link||null,via:o.via||null})),...n.filter(o=>w.has(o.data?.key)&&ve.includes(o.kind)).map(o=>({ts:o.ts,who:"love",via:"site",kind:o.kind,text:o.data.text||S(wt,o.kind)||"",files:o.data.files||[],eventId:o.id,reply:o.reply})),...n.filter(o=>o.kind==="owner-answer"&&g.has(o.data?.id)).map(o=>{const A=[o.data.label||o.data.choice,o.data.note].filter(Boolean).join(" \xB7 ");return{ts:o.ts,who:"love",via:"questions",kind:o.kind,text:`${g.get(o.data.id).question}
3
- \u2192 ${A}`,files:o.data.files||[],eventId:o.id,reply:o.reply}}),...n.filter(o=>o.kind==="ask"&&y.includes(o.id)).flatMap(o=>[{ts:o.ts,who:"love",via:"ask",kind:o.kind,text:o.data?.text||"",files:o.data?.files||[],eventId:o.id,reply:o.reply,...p.includes(o.id)?{moved:!0}:{},...typeof o.data?.from?.title=="string"?{from:o.data.from.title}:{}},...m.map(A=>({ts:o.ts,who:"claude",kind:"ask-link",text:`${A.became?"Became":"Added to"} ${A.ref}`,link:`#item=${encodeURIComponent(A.key)}`,html:null,via:null}))])].sort((o,A)=>String(o.ts).localeCompare(String(A.ts))),B=v.filter(o=>o.who==="claude"&&o.kind!=="status"&&o.kind!=="agent").map(o=>String(o.ts)).pop()||"",q=new Map(n.filter(o=>o.kind==="ask-edited").sort((o,A)=>String(o.ts).localeCompare(String(A.ts))).map(o=>[o.data?.id,o.data?.text]));for(const o of v){if(!o.eventId)continue;(o.kind==="ask"||o.kind==="item-message")&&typeof q.get(o.eventId)=="string"&&Object.assign(o,{text:q.get(o.eventId),edited:!0});const A=o.kind==="ask"&&(m.length>0||!!o.moved);o.state=o.reply?"replied":A||B>String(o.ts)?"answered":S(i,o.eventId)?"working":"waiting",o.canDelete=o.kind==="item-message"&&(o.state==="working"||o.state==="waiting"),o.canEdit=o.canDelete}return v}d(xe,"threadOf");function xt(t){const{bridge:s={},owner:n={},topicItems:l={},events:i=[],threads:u={},prs:m={},working:r={}}=t,p=s.config?.slack_domain,y=s.config?.self_dm,w=d(e=>e&&y&&p?`https://${p}.slack.com/archives/${y}/p${e.replace(".","")}`:null,"permalink"),g=t.now??Date.now(),v=i.filter(e=>e.data?.key&&ve.includes(e.kind)),B=new Set(i.filter(e=>e.kind==="ask-withdrawn"&&!e.data?.key).map(e=>e.data?.id)),q=new Map(i.filter(e=>e.kind==="ask"&&!B.has(e.id)).map(e=>[e.id,e])),o=d((e,a)=>new Map(i.filter(c=>c.kind===e).sort((c,O)=>String(c.ts).localeCompare(String(O.ts))).map(c=>[c.data?.[a],c.ts])),"latest"),A=o("ask-bump","id"),E=o("card-bump","key"),j=o("card-done","key"),Q=o("card-merge","key"),U=t.autoMerge===!0,x=t.tickets,P=new Map(i.filter(e=>e.kind==="card-hold").sort((e,a)=>String(e.ts).localeCompare(String(a.ts))).map(e=>[e.data?.key,e])),f=new Map,G=t.askLinks||{},W=new Map;for(const[e,a]of Object.entries(G))q.has(e)&&W.set(a,[...W.get(a)||[],e]);const K=[],$=[],F=new Map;for(const{slug:e,wave:a,agents:c={}}of t.waves){const O=t.github||a.repo||null;K.push({slug:e,title:a.title||e,status:a.status||null,internal:!!a.internal,finished:null,watched:!t.fromWaves||(t.watched||[]).includes(e),reviewers:(a.reviewers||[]).map(h=>h.name)}),a.items.forEach((h,_)=>{const R=`${e}/${h.id}`,me=W.get(R)||[],ne=[h.from_ask,...me].filter(b=>q.has(b)),Le=new Set(ne.map(b=>`ask:${b}`)),ke=[...S(u,R)||[],...ne.flatMap(b=>S(u,`ask:${b}`)||[])];h.from_ask&&f.set(h.from_ask,[...f.get(h.from_ask)||[],{key:R,ref:ae(a,h.id,x),became:!0}]);const ge=xe(R,{lines:ke,events:i,owner:n,working:r,fromAsk:h.from_ask||null,moved:me});for(const b of ge)b.eventId&&F.set(b.eventId,b.state);$.push(st({slug:e,wave:a,it:h,order:_,repo:O,pr:h.pr&&O&&S(m,`${O}#${h.pr}`)||null,...(b=>({agents:b,builder:mt(b,S(u,R),g),eta:b.map(je=>je.eta).filter(Boolean).sort().pop()||null}))(Ee(S(c,h.id))),thread:ke,events:[...v.filter(b=>b.data.key===R||Le.has(b.data.key)),...ne.map(b=>q.get(b))],working:ge.some(b=>b.state==="working"),topicItem:S(l,R),bridgeItem:S(s.items,R),permalink:w,now:g,bumpedAt:E.get(R),holdAsk:P.get(R),closedAt:j.get(R),mergeAt:Q.get(R),autoMerge:U,tickets:x}))}),(a.followups||[]).forEach((h,_)=>$.push(pt({slug:e,wave:a,f:h,order:a.items.length+_,repo:O,bumpedAt:E.get(`${e}/${h.id}`),closedAt:j.get(`${e}/${h.id}`),tickets:x})))}const I=new Map($.map(e=>[e.key,e])),D=new Map,V=t.try||{},T=["web","app"].includes(V.kind)?V.kind:t.waves.some(({wave:e})=>e.items.some(a=>a.install?.url))?"app":"web",H=`/${t.route||""}/try/`.replace(/^\/\//,"/");for(const e of $){if(e.kind!=="work"||e.lane==="done")continue;const a=S(V.cards,e.key)||{},c=a.head||e.pr?.headRefOid,O=T==="web"?a.sha:e.install?.sha,h=e.builder?.kind===te.install?"running":a.state||(T==="app"&&e.install?"ready":null);e.try={kind:T,state:h,stale:!!(O&&c&&!String(c).toLowerCase().startsWith(String(O).toLowerCase())),url:T==="web"?h==="ready"?`${H}${e.id.toLowerCase()}/`:null:e.install?.url||null,sha:O||null,changes:a.ahead>0,script:a.script===!0}}const N=V.main,J=N?.script&&N.sha?{slug:N.slug,branch:N.branch||"main",sha:N.sha.slice(0,7),at:N.at,kind:T,state:N.state||null,url:T==="web"?`${H}main/`:t.main?.install?.url||null}:null,Y=t.askProgress||{},Z=o("ask-done","id"),ee=i.filter(e=>e.kind==="owner-answer"),M=(n.items||[]).map(e=>kt(e,ee)),k=d(e=>{const a=M.filter(c=>c.topic===e);return{open:a.filter(c=>c.state==="open").length,answered:a.filter(c=>c.state==="answered").length}},"pendingOn");for(const e of q.values()){const a=`ask:${e.id}`,c=I.get(S(G,e.id)),O=[...f.get(e.id)||[]];if(c&&!O.some(_=>_.key===c.key)&&O.push({key:c.key,ref:c.ref,became:!1}),O.length){D.set(a,O[0].key);continue}const h=xe(a,{lines:S(u,a)||[],events:i,working:r,links:O});for(const _ of h)_.eventId&&F.set(_.eventId,_.state);$.push(ft(e,h,A.get(e.id),O.length>0||!!S(G,e.id),S(Y,e.id),Z.get(e.id),k(`item:${a}`)))}for(const e of K){const a=$.filter(c=>c.wave===e.slug);if(e.finished=ze(a),e.finished)for(const c of a)c.lane==="done"&&(c.fold=Ge(e.slug))}for(const e of $)e.kind==="ask"&&e.lane==="done"&&(e.fold=Ke);const z=new Set((n.topics||[]).map(e=>e.id)),le=d(()=>gt([...n.topics||[],...$.filter(e=>!z.has(`item:${e.key}`)).map(e=>({id:`item:${e.key}`,title:`${e.ref}: ${e.title}`,summary:"Questions about this work item."}))],M,new Set($.filter(e=>e.lane==="done").map(e=>`item:${e.key}`))),"topicsOf"),Ce=new Map(le().map(e=>[e.id,e]));for(const e of $){const a=Ce.get(`item:${e.key}`);if(!a){e.questions=null;continue}const c=a.questions.filter(h=>M.find(_=>_.id===h)?.state!=="resolved"),O=a.questions.find(h=>M.find(_=>_.id===h)?.state==="open")||c[0]||a.questions[0];e.questions={topic:a.id,state:a.state,open:a.open,answered:a.answered,replied:a.replied,resolved:a.resolved,first:O}}const re=new Map($.map(e=>[e.key,e])),Ie=new Map(M.map(e=>[e.id,e]));for(const e of $){e.blockedBy=e.kind!=="work"||e.lane==="done"?[]:e.deps.map(c=>re.get(String(c).includes("/")?c:`${e.wave}/${c}`)).filter(c=>c&&c.lane!=="done").map(c=>({key:c.key,ref:c.ref})),e.lane==="done"&&!e.fold&&e.updatedAt&&g-Date.parse(e.updatedAt)>=Fe&&(e.fold=Qe),ut(e,Ie,U),e.lane==="todo"&&e.kind==="work"&&!e.held&&ct(e)&&(e.lane="progress");const a=e.created?Math.floor((g-Date.parse(e.created))/se):0;e.todoAge=e.lane==="todo"&&a>=2?`${a} days ago`:null}const ie=d(e=>e.bumpedAt||"","bumped"),de=d(e=>e.isNew?0:1,"unsorted"),De=d((e,a)=>e.isNew?String(e.updatedAt).localeCompare(String(a.updatedAt)):String(a.updatedAt||"").localeCompare(String(e.updatedAt||"")),"age"),ue=d(e=>e.lane==="todo"&&e.kind==="followup"?1:0,"late");$.sort((e,a)=>be[e.lane]-be[a.lane]||de(e)-de(a)||ie(a).localeCompare(ie(e))||ue(e)-ue(a)||De(e,a)||a.order-e.order||e.key.localeCompare(a.key));const ce=le(),pe=[...vt(t.prompts),...yt($,ce,M)],fe=new Map((n.items||[]).map(e=>[e.id,e.topic])),qe=d(e=>e.kind==="owner-answer"?fe.has(e.data?.id)?/^item:(.+)$/.exec(fe.get(e.data.id))?.[1]||null:"":e.kind==="ask"?`ask:${e.id}`:e.data?.key||(e.kind.startsWith("ask-")&&e.data?.id?`ask:${e.data.id}`:null),"ownCardOf"),_e=d(e=>{const a=qe(e);return D.get(a)||a},"cardOf"),Re=d(e=>{const a=e.startsWith("ask:")?e.slice(4):null,c=a&&S(Y,a)?.state==="done"?S(Y,a).at:null;return[j.get(e),a&&Z.get(a),c].filter(Boolean).sort().pop()||""},"closedAt"),Ne=d(e=>{const a=_e(e);return a===null||re.has(a)&&Re(a)<String(S(r,e.id))},"live"),Te=i.filter(e=>S(r,e.id)&&Ne(e)&&(F.has(e.id)?F.get(e.id)==="working":!e.reply)).length,Me=$.filter(e=>e.ask?.answerAt).sort((e,a)=>a.ask.answerAt.localeCompare(e.ask.answerAt)).map(e=>({key:e.key,id:e.id,text:e.title,at:e.ask.answerAt}));return{kind:T,main:J,waves:K,items:$,moved:Object.fromEntries(D),answers:Me,owner:M,ownerTopics:ce,needs:pe,needsSummary:bt(pe),working:Te,checks:ht(t,g)}}d(xt,"buildBoard");const X=d((t,s)=>`${t} ${s}${t===1?"":"s"}`,"plural");function ht({checked:t,listen:s,watcher:n,replyAt:l,threads:i={},working:u={},waves:m=[]},r){const p=Number(t?.every)||30,w=[l,n?.since,...Object.values(u),...Object.values(i).flat().filter(v=>(v.who||"claude")==="claude").map(v=>v.ts),...m.flatMap(v=>Object.values(v.agents||{}).flatMap(B=>Object.values(B||{})).map(B=>B?.done||B?.started))].filter(v=>typeof v=="string"&&!Number.isNaN(Date.parse(v))).sort((v,B)=>Date.parse(v)-Date.parse(B)).pop()||null,g=!!t?.idle;return{at:t?.at||null,every:g?null:p,idle:g,late:!g&&!!t?.at&&r-Date.parse(t.at)>2*p*6e4,watcher:{listening:"running",asleep:"asleep"}[s]||"stopped",claudeAt:w}}d(ht,"checksOf");function yt(t,s,n){const l=new Set(t.map(r=>r.questions?.topic).filter(Boolean)),i=d(r=>r.find(p=>n.find(y=>y.id===p)?.state==="open")||r[0],"firstOpen"),u=s.filter(r=>r.state!=="resolved"&&r.open>0&&!l.has(r.id)).map(r=>{const p=`questions.html#${i(r.questions)}`;return{kind:"Question",title:r.title,text:`${r.title} \xB7 ${X(r.open,"question")}`,url:p,topic:r.id,questions:{open:r.open,url:p},ready:null}}),m=t.filter(r=>!r.held).map(r=>{const p=r.questions&&r.questions.state!=="resolved"&&r.questions.open>0?{open:r.questions.open,url:`questions.html#${r.questions.first}`}:null,y=r.waitsOn==="ready"?r.pr.url:null,w=!p&&!y&&r.action?{label:r.action.label,url:r.action.url}:null;if(!p&&!y&&!w)return null;const g=`${r.ref}: ${r.title}`,v=[g,p&&X(p.open,"question"),y&&"Ready on GitHub",w?.label].filter(Boolean).join(" \xB7 ");return{kind:p?"Question":y?"Tap Ready":"Action needed",title:g,text:v,url:`#item=${encodeURIComponent(r.key)}`,key:r.key,questions:p,ready:y,action:w}}).filter(Boolean);return[...u,...m]}d(yt,"needsOf");function vt(t=[]){return t.map(s=>{const n=s.questions?.length?`Question: ${s.questions.map(l=>l.question).join(" \xB7 ")}`:`${s.tool}: ${s.detail||s.action}`;return{kind:"Prompt",title:n,text:n,url:"",prompt:s}})}d(vt,"promptLines");function bt(t){const s=t.filter(i=>i.prompt).length,n=t.reduce((i,u)=>i+(u.questions?.open||0),0),l=t.filter(i=>i.ready).length;return[s&&X(s,"prompt"),n&&X(n,"question"),l&&`${X(l,"draft")} waiting on Ready`].filter(Boolean).join(" \xB7 ")}d(bt,"needsSummary");function Ct(t){const s=d((...n)=>t.items.filter(l=>n.includes(l.lane)).length,"count");return{items:t.needs.map(({kind:n,text:l,url:i})=>({kind:n,text:l,url:i})),working:t.working,summary:t.needsSummary,lanes:{review:s("review","stale"),progress:s("progress"),todo:s("todo")},building:t.items.some(n=>(n.agents||[]).some(l=>!l.done))}}d(Ct,"needsAnswer");export{C as ACTION_KIND,Ke as ASKS_FOLD,te as BUILDER_KIND,We as BUILDER_STALLED_AFTER,L as BUTTON,ve as ITEM_EVENTS,Bt as ITEM_STATUSES,Qe as OLDER_FOLD,xt as buildBoard,Ct as needsAnswer,kt as ownerQuestion,ae as refOf,xe as threadOf,Ge as waveFold};
1
+ var et=Object.defineProperty;var u=(e,n)=>et(e,"name",{value:n,configurable:!0});import{routeRecord as E}from"../hub/routes.mjs";import tt from"node:fs";import{ticketBadge as Re,ticketIn as nt}from"./tickets.mjs";import{reviewLinks as st,reviewTime as ot,reviewersOf as rt}from"./review.mjs";import{remoteFromInput as at}from"../hub/repos.mjs";import Me from"node:vm";const xe=Me.createContext({});for(const e of["ui-enums.js","ui-order.js"])Me.runInContext(tt.readFileSync(new URL(`./public/${e}`,import.meta.url),"utf8"),xe);const{BUTTON:G,ACTION_KIND:C,BUILDER_KIND:me}=xe.BoardUI,{laneOrder:lt}=xe.BoardUI,Ne=["item-message","mark-ready","grill-more","prototype-request"],ln=["planned","waiting","building","fixing","checking","held","merged","done","dropped"],it=30*6e4,dt="build-refused",ut=864e5,ke=u(e=>e.lane==="done"||e.lane==="asks","closedLane"),ct=u(e=>`wave:${e}`,"waveFold"),pt=7*ut,Le=u((e,n)=>({id:e,url:n?.workspace?`https://linear.app/${n.workspace}/issue/${e}`:null}),"linearRef"),ft=u((e,n)=>String(e).replace(/^S(\d+)$/,`${n||"INT"}-$1`),"internalRef"),Be=u((e,n,s)=>nt(s,n.ticket)?n.ticket:e.internal===!0?ft(n.id,s?.prefix):n.id,"refOf"),gt=u((e,n,s,o)=>Re(e||(n?{source:"internal",prefix:"INT"}:null),s,o)||(n?{id:String(n),url:null,source:null}:null),"ticketOf");function mt(e,n,s,o){const r=(e.status||"").toLowerCase();return r==="merged"||r==="done"||r==="dropped"||n?.state==="MERGED"||n?.state==="CLOSED"?"done":s||r==="checking"?"progress":o?"review":n&&n.state==="OPEN"&&!n.isDraft?n.checks?.state==="fail"?"progress":"review":n?"progress":"todo"}u(mt,"laneOf");const Te=u(e=>{const n=(e.status||"").toLowerCase();return n==="dropped"||e.pr?.state==="CLOSED"&&n!=="merged"&&n!=="done"},"droppedCard");function kt(e){if(!e.some(o=>o.kind==="work")||e.some(o=>o.lane!=="done"))return null;const n=e.filter(o=>o.lane==="done"),s=n.filter(Te).length;return{merged:n.length-s,dropped:s}}u(kt,"finishedOf");function wt(e,n,s){return Object.fromEntries(e.map(o=>{const r=E(n?.reviews,o.github),d=r==="APPROVED"?"approved":r==="CHANGES_REQUESTED"?"changes":Object.hasOwn(s,o.name)?"sent":"unsent";return[o.name,{state:d,since:d==="sent"?E(s,o.name):null,github:o.github||null}]}))}u(wt,"reviewOf");function ht(e,n,s,o,r){return s?.kind===me.install?"builder":o==="checking"?"checking":s?"builder":r&&e==="review"?"owner":e==="progress"&&n?.state==="OPEN"&&n.isDraft&&n.checks?.state==="pass"?"ready":e==="review"||e==="stale"?n?.reviewDecision==="APPROVED"?"merge":"reviewers":null}u(ht,"waitsOnOf");const Ae=/(^|\/)prototype-[\w-]+\.html/,yt={reviewers:"Waiting for PR reviewers",merge:"Approved, merges once CI and the merge state allow"},je={waiting:"Approved, Merge shows once CI and the merge state allow",ready:"Approved and green: tap Merge",merging:"Merging"},bt="Fix the failing CI",Pe="Tap Done once it is settled",Ue=u(e=>(e.who==="claude"||e.via==="github")&&!["status","agent","ask-link"].includes(e.kind),"countsUnread");function vt({slug:e,wave:n,it:s,order:o,pr:r,repo:d,remote:g,base:c,builder:l,agents:f,worktree:h,eta:i,thread:m,events:S,working:U,said:B,askedAt:K,topicItems:q,boardReviewers:a,now:$,bumpedAt:Q,holdAsk:L,closedAt:T,mergeAt:y,autoMerge:O,tickets:_}){const ne=s.status==="merged"||!!s.merged_at||r?.state==="MERGED"||r?.state==="CLOSED",X=!!T&&String(T)>String(s.status_at||"");X&&(s={...s,status:"done"});const se=String((X?T:s.status_at)||""),M=s.status==="done"&&!ne?[...B.filter(k=>k.kind==="item-message").map(k=>k.ts),...K].map(String).filter(k=>k>se).sort().pop():void 0;M&&(s={...s,status:void 0,next:void 0});const he=!!M&&B.some(k=>String(k.ts)>se&&(k.state==="waiting"||k.state==="working")),W=`${e}/${s.id}`,V=rt(n,a),le=Object.fromEntries(Object.entries(st(n,s,W,q)).map(([k,ae])=>[k,ot(ae.ts)])),R=Object.values(le).filter(Boolean).sort()[0]||null,N=[...m.map(k=>k.ts),...S.map(k=>k.ts)].sort().pop()||null,ye=[...m.filter(k=>Ue({...k,who:k.who||"claude"})).map(k=>k.ts),...S.map(k=>k.reply?.at)].filter(Boolean).sort(),Y=S.filter(k=>k.kind==="grill-more").pop(),F=!s.pr&&s.pushed?.branch?{branch:s.pushed.branch,at:s.pushed.at||null,host:g?.host||null}:null;let x=mt(s,r,f.some(k=>k.started&&!k.done&&te(k)),F);M&&x==="todo"&&(x="progress"),x==="review"&&R&&$-Date.parse(R)>pt&&(x="stale");let I=ht(x,r,l,(s.status||"").toLowerCase(),F);const j=I==="builder"&&r?.state==="OPEN"&&!r.isDraft,ie=x==="progress"&&!I&&r?.state==="OPEN"&&!r.isDraft,z=!!M&&x==="progress"&&!I&&!ie&&!he;z&&(I="owner");const oe=L&&String(L.ts)>String(s.held_at||""),J=x==="todo"&&(oe?!!L.data.hold:s.status==="held"||!!s.held),be=J&&(oe?L.data.until:s.pick_up_at)||null,Z=x==="todo"||!J&&x==="progress"&&I!=="ready"&&I!=="checking",ve=m.filter(k=>(k.who||"claude")==="claude"&&k.kind!=="status").map(k=>String(k.ts)).sort().pop()||"",de=I==="merge"&&!!y&&String(y)>ve,re=m.filter(k=>k.kind!=="status").sort((k,ae)=>String(k.ts).localeCompare(String(ae.ts))).at(-1),ue=x==="todo"&&re?.kind===dt?re.text:null,ce=M||f.filter(te).map(k=>k.started).filter(Boolean).map(String).sort().pop()||s.queued_at||["building","fixing","checking"].includes(String(s.status||"").toLowerCase())&&s.status_at||null,pe=[X&&T,s.status_at,s.merged_at,r?.mergedAt].filter(Boolean).map(String).sort().pop()||null,fe=F&&I==="owner"?`merge ${F.branch} into ${c}`:I==="merge"&&!O?je[de?"merging":"waiting"]:yt[I];return{key:W,kind:"work",order:o,wave:e,waveTitle:n.title||e,id:s.id,ref:Be(n,s,_),title:s.title,status:s.status||null,created:s.created||null,progressSince:ce,closedAt:x==="done"?pe:null,internal:n.internal===!0,repo:n.internal===!0&&!n.repo?null:d,lane:x,waitsOn:I,builder:I==="checking"?null:l,agents:f,worktree:h??null,eta:i,working:U,reopened:!!M,awaitsDone:z,queued:x!=="done"&&!!s.queued_at,deps:s.deps||[],held:J,heldUntil:be,risky:!!s.risky,bumpable:Z,bumpedAt:Z&&Q||null,holdable:x!=="done",hasNotes:!!(s.plan||String(s.notes||"").trim()),finishable:x!=="done",merging:de,refusal:ue,manualSteps:s.manual_steps||[],fromAsk:s.from_ask||null,next:j||x==="done"||I==="checking"?null:ie?bt:fe||s.next||null,branch:s.branch||r?.headRefName||F?.branch||null,pushed:x==="done"?null:F,ticket:_?.source==="linear"&&!s.ticket&&s.linear?.length?null:gt(_,s.ticket,s.ticket||(n.internal?Be(n,s,_):s.linear?.[0]),{repo:d,key:W}),ticketSource:_?.source||null,linear:(s.linear||[]).map(k=>_?.source==="linear"&&Re(_,k,{key:W,repo:d})||Le(k,_)),pr:s.pr&&d?{number:s.pr,url:`https://github.com/${d}/pull/${s.pr}`,...r||{}}:null,reviewers:V.map(k=>k.name),review:wt(V,r,le),inReviewSince:R,threadCount:m.length+S.length,lastTs:N,othersAt:ye,grilling:!!(Y&&!Y.reply)||f.some(k=>k.kind==="grill"&&k.started&&!k.done),state:s.state||null,updatedAt:r?.mergedAt||r?.updatedAt||s.merged_at||l?.at||N||null,ask:null,fold:null,action:null,install:x==="done"?null:St(s.install,r)}}u(vt,"workItem");function St(e,n){if(!e?.url)return null;const s=e.sha?String(e.sha).toLowerCase():null,o=n?.headRefOid?String(n.headRefOid).toLowerCase():null;return{url:e.url,label:e.label||null,builtAt:e.built_at||null,sha:s,older:!!(s&&o&&!o.startsWith(s))}}u(St,"installOf");function Ot(e){if(e?.state!=="OPEN")return[`PR is ${String(e?.state||"unknown").toLowerCase()}`];const{state:n,pending:s,failing:o}=e.checks||{};return[e.isDraft&&"it is a draft",e.reviewDecision!=="APPROVED"&&`review: ${String(e.reviewDecision||"none").toLowerCase().replace(/_/g," ")}`,n==="fail"&&`failing: ${o?.length?o.join(", "):"a check"}`,n==="pending"&&`pending: ${s?H(s,"check"):"checks"}`,e.mergeStateStatus!=="CLEAN"&&`merge state: ${String(e.mergeStateStatus||"unknown").toLowerCase()}`].filter(Boolean)}u(Ot,"mergeBlockers");const $t=u(e=>!Ot(e).length,"mergeable");function xt(e,n,s){if(e.held||ke(e))return null;const o=`#item=${encodeURIComponent(e.key)}`;if(We(e)){const r=n.get(e.questions.first);return Ae.test(r?.url||"")?{kind:C.pick,url:`questions.html#${r.id}`,label:"Prototypes ready: pick a take"}:{kind:C.questions,url:`questions.html#${e.questions.first}`,label:`Answer ${H(e.questions.open,"question")}`}}return e.waitsOn==="ready"?{kind:C.ready,url:e.pr.url,label:"Tap Ready on GitHub"}:e.kind==="ask"&&e.waitsOn==="owner"?{kind:C.done,url:o,label:e.next||Pe}:e.waitsOn==="merge"&&e.manualSteps.length?{kind:C.steps,url:o,label:"Run the manual steps, then merge"}:e.waitsOn==="merge"&&!s&&!e.merging&&$t(e.pr)?{kind:C.merge,url:o,label:"Merge"}:null}u(xt,"actionOf");const We=u(e=>!!(e.questions&&e.questions.state!=="resolved"&&e.questions.open>0),"asking"),Fe=new Set([C.pick,C.questions]);function Bt(e){return ke(e)?null:We(e)?G.openQuestions:e.action?.kind===C.done?G.markDone:e.kind==="ask"?null:G.grill}u(Bt,"actionButtonOf");const At=u(e=>e.kind==="work"&&e.lane==="todo"&&!e.action&&!e.unpicked,"buildableOf");function Et(e,n){const o=e.filter(r=>r.topic===`item:${n}`&&Ae.test(r.url||"")).flatMap(r=>r.answers.map(d=>({q:r,a:d}))).sort((r,d)=>String(r.a.ts).localeCompare(String(d.a.ts))).at(-1);return o&&!(o.q.options||[]).some(r=>r.id===o.a.choice)?{id:o.q.id,note:o.a.note.replace(/\s+/g," ").trim()}:null}u(Et,"noTakePicked");const Ct="No take picked, so it isn't ready to implement",qt=Object.freeze({[C.pick]:G.openQuestions,[C.questions]:G.openQuestions,[C.ready]:G.github,[C.steps]:G.github,[C.merge]:G.merge,[C.done]:G.markDone});function It(e,n,s){e.action=xt(e,n,s),e.awaitsDone&&=!e.action,e.actionButton=Bt(e),e.resolvedBy=qt[e.action?.kind]||null,e.action?.kind===C.merge&&(e.next=je.ready),e.awaitsDone&&Object.assign(e,{lane:"review",next:Pe,bumpable:!1,bumpedAt:null});const o=e.lane==="todo"||e.lane==="progress"&&(e.waitsOn==="ready"||e.waitsOn==="owner");if(!e.action||!o)return;const r=e.lane==="todo"&&Fe.has(e.action.kind)?"todo":"review";Object.assign(e,{lane:r,waitsOn:e.waitsOn==="ready"?"ready":"owner",next:e.action.label}),r==="review"&&Object.assign(e,{bumpable:!1,bumpedAt:null})}u(It,"placeOwnerWait");const Ge=u(e=>e.queued||e.agents.some(te)||["building","fixing"].includes(String(e.status||"").toLowerCase()),"started"),_t="none set yet";function dn(e,n){const s=String(e.status||"").toLowerCase();if(e.next||e.pr||e.pushed||e.held||e.queued_at||["held","checking","done","merged","dropped"].includes(s))return!1;const o=n.filter(r=>r?.started);return!o.some(r=>!r.done)&&Ge({status:s,agents:o})}u(dn,"owesNext");function Dt(e,n=new Date){const s=new Date(e),o=s.toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"});return s.toDateString()===n.toDateString()?o:`${s.getMonth()+1}/${s.getDate()} ${o}`}u(Dt,"askedLabel");function Rt(e,n,s,o,r,d,g){const c=n.find(y=>y.kind==="ask"&&y.eventId===e.id),l=e.data?.files||[],f=(c.text||"").trim().split(`
2
+ `)[0]||l.map(y=>y.name).join(", ")||"Ask",h=c.state==="replied"||c.state==="answered",i=r?.state==="progress"&&!(String(d||"")>String(r.at||"")),m=!i&&(!!r||!!d),S=h&&!m&&!i&&g.open>0,U=h&&!m&&!i&&!S&&g.answered>0,B=m||h&&!i&&!S&&!U,K=h&&!o&&!r?.moved&&[...n.filter(y=>y.who==="claude"&&y.kind!=="ask-link"&&y.kind!=="status").map(y=>y.ts),...n.map(y=>y.reply?.at)].filter(Boolean).sort().pop()||null,q=B||i||S||U,a=n.some(y=>y.who==="love"&&y.kind!=="ask"&&(y.state==="waiting"||y.state==="working")),$=q?"answered":c.state,Q=q?null:s||null,L=[...n.filter(Ue).map(y=>y.ts),...n.map(y=>y.reply?.at)].filter(Boolean).sort(),T=[...n.map(y=>y.ts),...L,...i?[r.at]:[]].filter(Boolean).sort().pop();return{key:`ask:${e.id}`,kind:"ask",order:0,wave:null,waveTitle:null,id:e.id,ref:q?Dt(e.ts):"New",isNew:!q,title:f,status:null,internal:!1,repo:null,lane:i?"progress":B?"asks":"todo",waitsOn:i&&!a?"owner":null,builder:null,agents:[],eta:null,working:n.some(y=>y.state==="working"),deps:[],held:!1,risky:!1,bumpable:!q,bumpedAt:Q,manualSteps:[],fromAsk:null,from:null,next:i&&r.next||null,hasNotes:!1,branch:null,finishable:q&&!B,linear:[],pr:null,reviewers:[],review:{},inReviewSince:null,progressSince:i&&r.at||null,closedAt:null,threadCount:n.length,lastTs:T,othersAt:q?L:[],grilling:!1,created:e.ts,updatedAt:q?T:e.ts,ask:{eventId:e.id,state:$,askedAt:e.ts,bumpedAt:Q,edited:!!c.edited,canEdit:!q,text:c.text,files:l,answerAt:K,from:c.from||null},fold:null,action:null}}u(Rt,"askItem");const Mt=new Set(["grill","prototype"]),ee=u(e=>Mt.has(e.kind),"plans"),te=u(e=>!ee(e),"builds");function Ee(e={}){return Object.entries(e).filter(([,n])=>n&&typeof n=="object").map(([n,s])=>({name:n,kind:Qe(s.kind)||null,model:s.model||(s.kind==="opus"||s.kind==="sonnet"?s.kind:null),label:s.label||null,started:s.started||null,eta:s.done?null:s.eta||null,done:s.done||null,tapped:s.tapped===!0,calls:Array.isArray(s.calls)?s.calls:[],verified:s.verified||null})).sort((n,s)=>String(n.started||"").localeCompare(String(s.started||""))||n.name.localeCompare(s.name))}u(Ee,"agentsOf");function Nt(e={}){return Object.values(e).filter(n=>n?.worktree?.path).sort((n,s)=>String(n.started||"").localeCompare(String(s.started||""))).at(-1)?.worktree.path??null}u(Nt,"worktreeOf");function Lt(e,n=[],s){const o=e.filter(i=>i.started&&!i.done),r=o.every(ee),d=r?o:o.filter(te),g=d.map(i=>String(i.started||"")).sort()[0];if(g===void 0)return null;const c=n.filter(i=>i.kind==="status"&&String(i.ts)>=g&&(r||!i.planner)).sort((i,m)=>String(i.ts).localeCompare(String(m.ts))).pop(),l=d.some(i=>i.kind===me.install)?me.install:Qe(d.at(-1).kind),f=c?.ts||g;return{progress:c?c.phase?c.text.replace(": "," \xB7 "):c.text||null:null,phase:c?.phase||null,waiting:!c&&d.some(i=>i.tapped),at:f,kind:l,planner:r,silentSince:f,stalled:s-Date.parse(f)>=it}}u(Lt,"builderOf");const Ce={opus:"Claude Opus",sonnet:"Claude Sonnet",codex:"Codex"},Tt=u(({model:e,kind:n})=>Ce[e]||Ce[n]||(n==="claude"?Ce.opus:null),"modelName"),Qe=u(e=>e==="opus"||e==="sonnet"?"claude":e,"providerOf");function qe(e){const n=Tt(e),{kind:s}=e,o=e.label?.replace(/\bbuilder\b/,"implementer").replace(/\bBuilder\b/,"Implementer"),r=u(g=>g[0].toLowerCase()+g.slice(1),"lower");if(n&&ee(e))return`${n} ${s}`;if(n)return`${n} ${r(o?.replace(/^(?:claude\s+)?(?:opus|sonnet|codex)\b\s*/i,"")||"implementer")}`;const d=s?s[0].toUpperCase()+s.slice(1):"";return d?o?o.toLowerCase().includes(s.toLowerCase())?o:`${d} ${r(o)}`:d:o||"Implementer"}u(qe,"agentName");const Ke=u((e,n)=>!!e.resolved&&(!e.resolved_at||String(e.resolved_at)>=String(n)),"resolvedSince");function jt(e,n,s=[]){const o=n.filter(f=>f.data?.id===e.id),r=o[o.length-1]||null,d=!!(e.resolved&&e.resolved_at&&r&&String(r.ts)>e.resolved_at),g=e.resolved&&!d,c=g?null:s.filter(f=>f.data?.question===e.id&&String(f.ts)>String(r?.ts||"")).at(-1),l=g?"resolved":c?"requested":r?r.reply?"replied":"answered":"open";return{...e,state:l,request:c?{id:c.id,ts:c.ts,note:c.data.note||"",choice:c.data.choice||null}:null,answers:o.map(f=>({ts:f.ts,choice:f.data.choice||null,note:f.data.note||"",files:f.data.files||[],reply:f.reply,acted:Ke(e,f.ts)}))}}u(jt,"ownerQuestion");const we=u(e=>e.round??(Ae.test(e.url||"")?1:0),"roundOf");function Pt(e,n){const s=u((o,r)=>o.confirm_of||(o.options||[]).some(d=>d.id===r.choice),"settles");return e.filter(o=>we(o)===n&&o.answers.length).map(o=>[o,o.answers.at(-1)]).filter(([o,r])=>s(o,r)).map(([,o])=>o).sort((o,r)=>String(o.ts).localeCompare(String(r.ts))).at(-1)}u(Pt,"roundSettledBy");function Ut(e){const n=e.find(r=>r.state==="requested");if(n)return{n:we(n)+1,state:"making"};const s=Math.max(0,...e.map(we));if(!s)return null;const o=Pt(e,s);return o?{n:s,state:"closed",take:o.choice}:e.some(r=>we(r)===s&&r.state!=="resolved")?{n:s,state:"back"}:null}u(Ut,"roundState");function Wt(e,n,s=new Set){const o=new Set(e.map(d=>d.id)),r=[...e];return n.some(d=>!o.has(d.topic))&&r.push({id:"",title:"Other"}),r.map(d=>{const g=n.filter(h=>(o.has(h.topic)?h.topic:"")===d.id),c=u(h=>g.filter(i=>i.state===h).length,"count"),l=g.every(h=>h.state==="resolved")||s.has(d.id)&&!c("open"),f=g.length&&l?"resolved":"open";return{...d,state:f,questions:g.map(h=>h.id),open:c("open"),answered:c("answered"),replied:c("replied"),requested:c("requested"),resolved:c("resolved"),round:Ut(g)}}).filter(d=>d.questions.length)}u(Wt,"ownerTopics");const Ft={"mark-ready":"Mark ready","grill-more":"Grill please","prototype-request":"Prototype please"},Gt=u((e,n)=>{if(e.kind!=="prototype-request"||!n.has(e.data.question))return e.data.text||E(Ft,e.kind)||"";const s=n.get(e.data.question),o=(s.options||[]).find(r=>r.id===e.data.choice);return[`Prototype please:${s.question}`,o&&`${o.id}. ${o.label}`,e.data.note].filter(Boolean).join(`
3
+ \u2192 `)},"actionText"),Ie=u(e=>[...e.filter(n=>n.who==="claude"&&n.kind!=="status"&&n.kind!=="agent").map(n=>n.ts),...e.filter(n=>n.kind==="ask").map(n=>n.reply?.at)].filter(Boolean).map(String).sort().pop()||"","claudeAtOf");function Ve(e,{lines:n=[],events:s=[],owner:o={},working:r={},fromAsk:d=null,links:g=[],agents:c={},moved:l=[]}){const f=e.startsWith("ask:")?[e.slice(4)]:[d,...l].filter(Boolean),h=new Set([e,...f.map(a=>`ask:${a}`)]),i=new Map((o.items||[]).filter(a=>a.topic===`item:${e}`).map(a=>[a.id,a])),m=new Set([...i.values()].map(a=>a.round_of).filter(Boolean)),S=Ee(c).filter(te).map(a=>String(a.started||"")).filter(Boolean),U=new Set(s.filter(a=>a.kind==="owner-answer"&&i.has(a.data?.id)).filter(a=>Ke(i.get(a.data.id),a.ts)||m.has(a.data.id)||S.some($=>$>String(a.ts))).map(a=>a.id)),B=[...Ee(c).flatMap(a=>[...a.started?[{ts:a.started,who:"claude",kind:"agent",text:`${qe(a)} started`,finished:!1,eta:a.done?null:a.eta,...ee(a)?{planner:!0}:{}}]:[],...a.done?[{ts:a.done,who:"claude",kind:"agent",text:`${qe(a)} finished`,finished:!0,eta:null,...ee(a)?{planner:!0}:{}}]:[],...a.calls.length?[{ts:a.done||a.calls.at(-1).at,who:"claude",kind:"calls",text:qe(a),calls:a.calls.map($=>$.text),verified:a.verified}]:[]]),...n.map(a=>({ts:a.ts,who:a.who||"claude",...a.kind==="status"?{kind:"status",...a.planner?{planner:!0}:{}}:{},text:a.text||"",html:a.html||null,link:a.link||null,via:a.via||null})),...s.filter(a=>h.has(a.data?.key)&&Ne.includes(a.kind)).map(a=>({ts:a.ts,who:"love",via:"site",kind:a.kind,text:Gt(a,i),files:a.data.files||[],eventId:a.id,reply:a.reply})),...s.filter(a=>a.kind==="owner-answer"&&i.has(a.data?.id)).map(a=>{const $=[a.data.label||a.data.choice,a.data.note].filter(Boolean).join(" \xB7 ");return{ts:a.ts,who:"love",via:"questions",kind:a.kind,text:`${i.get(a.data.id).question}
4
+ \u2192 ${$}`,files:a.data.files||[],eventId:a.id,reply:a.reply}}),...s.filter(a=>a.kind==="ask"&&f.includes(a.id)).flatMap(a=>[{ts:a.ts,who:"love",via:"ask",kind:a.kind,text:a.data?.text||"",files:a.data?.files||[],eventId:a.id,reply:a.reply,...l.includes(a.id)?{moved:!0}:{},...typeof a.data?.from?.title=="string"?{from:a.data.from.title}:{}},...g.map($=>({ts:a.ts,who:"claude",kind:"ask-link",text:`${$.became?"Became":"Added to"} ${$.ref}`,link:`#item=${encodeURIComponent($.key)}`,html:null,via:null}))])].sort((a,$)=>String(a.ts).localeCompare(String($.ts))),K=Ie(B),q=new Map(s.filter(a=>a.kind==="ask-edited").sort((a,$)=>String(a.ts).localeCompare(String($.ts))).map(a=>[a.data?.id,a.data?.text]));for(const a of B){if(!a.eventId)continue;(a.kind==="ask"||a.kind==="item-message")&&typeof q.get(a.eventId)=="string"&&Object.assign(a,{text:q.get(a.eventId),edited:!0});const $=a.kind==="ask"&&(g.length>0||!!a.moved);a.state=a.reply?"replied":$||U.has(a.eventId)||K>String(a.ts)?"answered":E(r,a.eventId)?"working":"waiting",a.canDelete=a.kind==="item-message"&&(a.state==="working"||a.state==="waiting"),a.canEdit=a.canDelete}return B}u(Ve,"threadOf");function Qt(e,n,s,o,r,d){const g=u((i,m)=>{const S=d.filter(B=>B.kind==="wave-triage"&&B.data?.[i]===m).pop(),U=r.filter(B=>B.topic===`${i}:${m}`&&(B.state==="open"||B.state==="answered")).length;return{running:!!(S&&!S.reply),questions:U}},"triageOf"),c=new Map(o.map(i=>[i.key,i])),l=u(i=>i.filter(m=>m.lane==="done"&&!Te(m)).length,"merged"),f=s.map(i=>({l:i,cards:i.keys.map(m=>c.get(m)).filter(Boolean)})).map(i=>({...i,open:!i.l.finished_at&&i.cards.some(m=>m.lane!=="done")})).sort((i,m)=>Number(i.open)-Number(m.open));for(const{l:i,cards:m}of f)for(const S of m)S.lineup={slug:i.slug,title:i.title};const h=f.filter(i=>i.open).map(({l:i,cards:m})=>({kind:"lineup",slug:i.slug,title:i.title,goal:i.goal,order:m.map(S=>({key:S.key,ref:S.ref})),merged:l(m),total:m.length,triage:g("lineup",i.slug)}));return e.fromWaves?[...h,...n.filter(i=>!i.finished).map(i=>({w:i,cards:o.filter(m=>m.wave===i.slug&&m.kind!=="ask")})).filter(({cards:i})=>i.length).map(({w:i,cards:m})=>({kind:"wave",slug:i.slug,title:i.title,goal:null,order:null,merged:l(m),total:m.length,triage:g("wave",i.slug)}))]:h}u(Qt,"waveRowsOf");function un(e){const{owner:n={},topicItems:s={},events:o=[],threads:r={},prs:d={},working:g={}}=e,c=e.now??Date.now(),l=o.filter(t=>t.data?.key&&Ne.includes(t.kind)),f=new Set(o.filter(t=>t.kind==="ask-withdrawn"&&!t.data?.key).map(t=>t.data?.id)),h=new Map(o.filter(t=>t.kind==="ask"&&!f.has(t.id)).map(t=>[t.id,t])),i=u((t,p)=>new Map(o.filter(w=>w.kind===t).sort((w,A)=>String(w.ts).localeCompare(String(A.ts))).map(w=>[w.data?.[p],w.ts])),"latest"),m=i("ask-bump","id"),S=i("card-unbump","key"),U=new Map([...i("card-bump","key")].filter(([t,p])=>!(String(S.get(t)||"")>String(p)))),B=i("card-done","key"),K=i("card-merge","key"),q=e.autoMerge===!0,a=e.tickets,$=new Map(o.filter(t=>t.kind==="card-hold").sort((t,p)=>String(t.ts).localeCompare(String(p.ts))).map(t=>[t.data?.key,t])),Q=new Map,L=e.askLinks||{},T=new Map;for(const[t,p]of Object.entries(L))h.has(t)&&T.set(p,[...T.get(p)||[],t]);const y=[],O=[],_=new Map,ne=new Map,X=new Map,se=new Map,M=e.github?null:at(e.remote);for(const{slug:t,wave:p,agents:w={}}of e.waves){const A=e.github||p.repo||null;y.push({slug:t,title:p.title||t,status:p.status||null,internal:!!p.internal,finished:null,reviewers:(p.reviewers||[]).map(v=>v.name)}),p.items.forEach((v,P)=>{const D=`${t}/${v.id}`,_e=T.get(D)||[],Oe=[v.from_ask,..._e].filter(b=>h.has(b)),ze=new Set(Oe.map(b=>`ask:${b}`)),De=[...E(r,D)||[],...Oe.flatMap(b=>E(r,`ask:${b}`)||[])];v.from_ask&&Q.set(v.from_ask,[...Q.get(v.from_ask)||[],{key:D,ref:Be(p,v,a),became:!0}]);const ge=Ve(D,{lines:De,events:o,owner:n,working:g,fromAsk:v.from_ask||null,moved:_e});for(const b of ge)b.eventId&&_.set(b.eventId,b.state);ne.set(D,Ie(ge));const Je=v.pr&&A&&E(d,`${A}#${v.pr}`)||null;O.push(vt({slug:t,wave:p,it:v,order:P,repo:A,remote:M,base:e.branch||"main",pr:Je,...(b=>({agents:b,builder:Lt(b,E(r,D),c),eta:b.map(Ze=>Ze.eta).filter(Boolean).sort().pop()||null}))(Ee(E(w,v.id))),worktree:Nt(E(w,v.id)),thread:De,events:[...l.filter(b=>b.data.key===D||ze.has(b.data.key)),...Oe.map(b=>h.get(b))],working:ge.some(b=>b.state==="working"),said:ge.filter(b=>b.who==="love"&&(b.kind==="item-message"||b.kind==="owner-answer")),askedAt:(n.items||[]).filter(b=>b.topic===`item:${D}`&&b.asked_at).map(b=>b.asked_at),topicItems:s,boardReviewers:e.reviewers||[],now:c,bumpedAt:U.get(D),holdAsk:$.get(D),closedAt:B.get(D),mergeAt:K.get(D),autoMerge:q,tickets:a}));const $e=O[O.length-1];se.set(D,{parent:v.parent?Xe(t,v.parent):null,related:(v.related||[]).map(b=>Xe(t,b))}),$e.lane==="done"&&X.set(D,$e.closedAt||String($e.updatedAt||""))})}const he=new Map(O.map(t=>[t.key,t])),W=new Map,V=e.try||{},le=e.autoBuild===!0,R=["web","app"].includes(V.kind)?V.kind:e.waves.some(({wave:t})=>t.items.some(p=>p.install?.url))?"app":"web";for(const t of O){if(t.kind!=="work"||t.lane==="done")continue;const p=E(V.cards,t.key)||{},w=p.head||t.pr?.headRefOid,A=R==="web"?p.sha:t.install?.sha,v=t.builder?.kind===me.install?"running":p.state||(R==="app"&&t.install?"ready":null);t.try={kind:R,state:v,stale:!!(A&&w&&!String(w).toLowerCase().startsWith(String(A).toLowerCase())),url:R==="web"?null:t.install?.url||null,port:R==="web"&&v==="ready"&&p.https||null,sha:A||null,changes:p.ahead>0,script:p.script===!0,autoBuild:le}}const N=V.main,ye=N?.script&&N.sha?{slug:N.slug,branch:N.branch||"main",sha:N.sha.slice(0,7),at:N.at,kind:R,state:N.state||null,url:R==="web"?null:e.main?.install?.url||null,port:R==="web"&&N.state==="ready"&&N.https||null}:null,Y=e.askProgress||{},F=i("ask-done","id"),x=o.filter(t=>t.kind==="owner-answer"),I=o.filter(t=>t.kind==="prototype-request"),j=(n.items||[]).map(t=>jt(t,x,I)),ie=u(t=>{const p=j.filter(w=>w.topic===t);return{open:p.filter(w=>w.state==="open").length,answered:p.filter(w=>w.state==="answered"||w.state==="requested").length}},"pendingOn");for(const t of h.values()){const p=`ask:${t.id}`,w=he.get(E(L,t.id)),A=[...Q.get(t.id)||[]];if(w&&!A.some(P=>P.key===w.key)&&A.push({key:w.key,ref:w.ref,became:!1}),A.length){W.set(p,A[0].key);continue}const v=Ve(p,{lines:E(r,p)||[],events:o,working:g,links:A});for(const P of v)P.eventId&&_.set(P.eventId,P.state);ne.set(p,Ie(v)),O.push(Rt(t,v,m.get(t.id),A.length>0||!!E(L,t.id),E(Y,t.id),F.get(t.id),ie(`item:${p}`)))}for(const t of y){const p=O.filter(w=>w.wave===t.slug);if(t.finished=kt(p),t.finished)for(const w of p)w.lane==="done"&&(w.fold=ct(t.slug))}for(const t of O)t.lineup=null;const z=new Set((n.topics||[]).map(t=>t.id)),oe=e.waves.flatMap(({slug:t,wave:p})=>(p.lineups||[]).map(w=>({...w,keys:w.items.map(A=>`${t}/${A}`)}))),J=u(()=>Wt([...n.topics||[],...O.filter(t=>!z.has(`item:${t.key}`)).map(t=>({id:`item:${t.key}`,title:`${t.ref}: ${t.title}`,...t.kind==="ask"?{askedAt:t.ask.askedAt}:{},summary:"Questions about this work item."})),...oe.filter(t=>!z.has(`lineup:${t.slug}`)).map(t=>({id:`lineup:${t.slug}`,title:t.title,summary:"Wave triage."})),...(e.fromWaves?y:[]).filter(t=>!z.has(`wave:${t.slug}`)).map(t=>({id:`wave:${t.slug}`,title:t.title,summary:"Wave triage."}))],j,new Set(O.filter(ke).map(t=>`item:${t.key}`))),"topicsOf"),be=new Map(J().map(t=>[t.id,t]));for(const t of O){const p=be.get(`item:${t.key}`);if(!p){t.questions=null,t.grilling||=t.state==="grilling";continue}const w=p.questions.filter(v=>j.find(P=>P.id===v)?.state!=="resolved"),A=p.questions.find(v=>j.find(P=>P.id===v)?.state==="open")||w[0]||p.questions[0];t.questions={topic:p.id,state:p.state,open:p.open,answered:p.answered,replied:p.replied,resolved:p.resolved,first:A}}const Z=new Map(O.map(t=>[t.key,t])),ve=new Map(j.map(t=>[t.id,t]));for(const t of O){t.blockedBy=t.kind!=="work"||t.lane==="done"?[]:t.deps.map(w=>Z.get(String(w).includes("/")?w:`${t.wave}/${w}`)).filter(w=>w&&w.lane!=="done"&&!w.reopened).map(He),It(t,ve,q),t.lane==="todo"&&t.kind==="work"&&!t.held&&Ge(t)&&(t.lane="progress"),t.unpicked=t.kind==="work"&&t.lane==="todo"&&!!Et(j,t.key),t.unpicked&&!t.action&&(t.next=Ct),t.buildable=At(t);const p=t.kind==="work"&&t.lane==="todo";t.unsorted=p&&!!t.fromAsk&&!t.state,t.ready=p&&t.state==="ready"&&!t.blockedBy.length&&!t.action,t.sortAt=Ht[t.lane](t)||t.updatedAt||null}Vt(O,Z,se),O.sort(lt);const de=Qt(e,y,oe,O,j,o),re=J(),ue=[...zt(e.prompts),...Yt(O,re,j)],ce=new Map((n.items||[]).map(t=>[t.id,t.topic])),pe=u(t=>t.kind==="owner-answer"?ce.has(t.data?.id)?/^item:(.+)$/.exec(ce.get(t.data.id))?.[1]||null:"":t.kind==="ask"?`ask:${t.id}`:t.data?.key||(t.kind.startsWith("ask-")&&t.data?.id?`ask:${t.data.id}`:null),"ownCardOf"),fe=u(t=>{const p=pe(t);return W.get(p)||p},"cardOf"),k=u(t=>{const p=t.startsWith("ask:")?t.slice(4):null,w=p&&E(Y,p)?.state==="done"?E(Y,p).at:null;return[B.get(t),p&&F.get(p),w,X.get(t)].filter(Boolean).sort().pop()||""},"closedAt"),ae=u(t=>{const p=fe(t);return p===null?!t.reply:!Z.has(p)||!(k(p)<String(E(g,t.id)))?!1:_.has(t.id)?_.get(t.id)==="working":!t.reply&&!W.has(pe(t))&&!(String(ne.get(p)||"")>String(t.ts))},"unanswered"),Se=o.filter(t=>E(g,t.id)&&ae(t)).map(t=>({id:t.id,kind:t.kind,key:fe(t),data:t.data,since:E(g,t.id)}));for(const t of O)t.working=Se.some(p=>p.key===t.key),t.kind==="work"&&t.lane==="progress"&&!t.builder&&!t.next&&t.waitsOn!=="checking"&&!t.working&&!t.queued&&(t.next=_t);const Ye=O.filter(t=>t.ask?.answerAt).sort((t,p)=>p.ask.answerAt.localeCompare(t.ask.answerAt)).map(t=>({key:t.key,id:t.id,text:t.title,at:t.ask.answerAt}));return{kind:R,main:ye,remote:M&&{host:M.host,name:`${M.host}/${M.path}`},waves:y,waveRows:de,items:O,moved:Object.fromEntries(W),answers:Ye,owner:j,ownerTopics:re,needs:ue,needsSummary:Jt(ue),working:Se.length,workingOn:Se,checks:Xt(e,c)}}u(un,"buildBoard");const He=u(e=>({key:e.key,ref:e.ref}),"linkOf"),Xe=u((e,n)=>String(n).includes("/")?String(n):`${e}/${n}`,"linkKey"),Kt=u((e,n)=>e.ref.localeCompare(n.ref,void 0,{numeric:!0}),"byRef");function Vt(e,n,s){const o=new Map,r=new Map,d=new Map,g=u((l,f,h)=>l.set(f,[...l.get(f)||[],h]),"add");for(const[l,f]of s){f.parent&&g(o,f.parent,l);for(const h of f.related)h!==l&&(g(r,l,h),g(r,h,l))}for(const l of e)for(const f of l.blockedBy||[])g(d,f.key,l.key);const c=u((l=[])=>[...new Set(l)].map(f=>n.get(f)).filter(Boolean).map(He).sort(Kt),"cards");for(const l of e){const f=s.get(l.key)?.parent;l.parent=f?{key:f,ref:n.get(f)?.ref||f.slice(f.indexOf("/")+1)}:null,l.followUp=!!f,l.children=c(o.get(l.key)),l.related=c(r.get(l.key)),l.waitsOnIt=c(d.get(l.key))}}u(Vt,"linkCards");const Ht={todo:u(e=>e.created,"todo"),progress:u(e=>e.progressSince,"progress"),review:u(e=>e.inReviewSince,"review"),stale:u(e=>e.inReviewSince,"stale"),done:u(e=>e.closedAt,"done"),asks:u(e=>e.ask.answerAt,"asks")},H=u((e,n)=>`${e} ${n}${e===1?"":"s"}`,"plural");function Xt({checked:e,listen:n,watcher:s,replyAt:o,threads:r={},working:d={},waves:g=[]},c){const l=Number(e?.every)||30,h=[o,s?.since,...Object.values(d),...Object.values(r).flat().filter(m=>(m.who||"claude")==="claude").map(m=>m.ts),...g.flatMap(m=>Object.values(m.agents||{}).flatMap(S=>Object.values(S||{})).map(S=>S?.done||S?.started))].filter(m=>typeof m=="string"&&!Number.isNaN(Date.parse(m))).sort((m,S)=>Date.parse(m)-Date.parse(S)).pop()||null,i=!!e?.idle;return{at:e?.at||null,every:i?null:l,idle:i,late:!i&&!!e?.at&&c-Date.parse(e.at)>2*l*6e4,watcher:{listening:"running",asleep:"asleep",waking:"waking"}[n]||"stopped",claudeAt:h}}u(Xt,"checksOf");function Yt(e,n,s){const o=new Set(e.map(c=>c.questions?.topic).filter(Boolean)),r=u(c=>c.find(l=>s.find(f=>f.id===l)?.state==="open")||c[0],"firstOpen"),d=n.filter(c=>c.state!=="resolved"&&c.open>0&&!o.has(c.id)).map(c=>{const l=`questions.html#${r(c.questions)}`,f=/^(?:lineup|wave):(.+)$/.exec(c.id)?.[1]||null;return{kind:"Question",title:c.title,text:`${c.title} \xB7 ${H(c.open,"question")}`,url:l,topic:c.id,waveRow:f,questions:{open:c.open,url:l},ready:null}}),g=e.filter(c=>!c.held).map(c=>{const l=c.questions&&c.questions.state!=="resolved"&&c.questions.open>0?{open:c.questions.open,url:`questions.html#${c.questions.first}`}:null,f=c.waitsOn==="ready"?c.pr.url:null,h=!l&&!f&&c.action?{label:c.action.label,url:c.action.url}:null;if(!l&&!f&&!h)return null;const i=`${c.ref}: ${c.title}`,m=[i,l&&H(l.open,"question"),f&&"Ready on GitHub",h?.label].filter(Boolean).join(" \xB7 ");return{kind:l?"Question":f?"Tap Ready":"Action needed",title:i,text:m,url:`#item=${encodeURIComponent(c.key)}`,key:c.key,questions:l,ready:f,action:h}}).filter(Boolean);return[...d,...g]}u(Yt,"needsOf");function zt(e=[]){return e.map(n=>{const s=n.questions?.length?`Question: ${n.questions.map(o=>o.question).join(" \xB7 ")}`:`${n.tool}: ${n.detail||n.action}`;return{kind:"Prompt",title:s,text:s,url:"",prompt:n}})}u(zt,"promptLines");function Jt(e){const n=e.filter(r=>r.prompt).length,s=e.reduce((r,d)=>r+(d.questions?.open||0),0),o=e.filter(r=>r.ready).length;return[n&&H(n,"prompt"),s&&H(s,"question"),o&&`${H(o,"draft")} waiting on Ready`].filter(Boolean).join(" \xB7 ")}u(Jt,"needsSummary");function cn(e){const n=u((...s)=>e.items.filter(o=>s.includes(o.lane)).length,"count");return{items:e.needs.map(({kind:s,text:o,url:r})=>({kind:s,text:o,url:r})),working:e.working,summary:e.needsSummary,lanes:{review:n("review","stale"),progress:n("progress"),todo:n("todo")},building:e.items.some(s=>(s.agents||[]).some(o=>!o.done))}}u(cn,"needsAnswer");function pn(e){const n=e.filter(l=>!ke(l)),s=u(l=>(l.agents||[]).filter(f=>!f.done),"running"),o=!n.some(l=>l.waitsOn==="checking"||s(l).some(te)),r=n.filter(l=>l.buildable&&[null,"ready"].includes(l.state??null)&&!l.unsorted&&!l.blockedBy.length&&!l.questions&&!l.grilling&&!l.working),d=[...r.filter(l=>l.state==="ready"),...r.filter(l=>l.state!=="ready")].map(l=>l.key),c=n.some(l=>l.grilling||s(l).some(ee))?[]:n.filter(l=>l.buildable&&l.state!=="ready"&&!l.held&&!l.questions?.open&&!l.questions?.answered&&!l.working).map(l=>l.key);return{idle:o,pullable:d,plannable:c}}u(pn,"pullOf");function fn(e){return e.filter(n=>n.lane==="todo").map(n=>JSON.stringify([n.key,n.title,n.deps||[]])).sort().join(`
5
+ `)}u(fn,"todoOf");const Zt=u(e=>e.startsWith("ask:")?`ask-${e.slice(4)}`:`item-${e}`,"pushTag");function gn(e){const n=u(d=>e.owner.filter(g=>g.topic===d&&g.state==="open").map(({id:g,question:c})=>({id:g,question:c})),"open"),s=[C.ready,C.merge],o=e.items.filter(d=>d.action).map(d=>{const g=Fe.has(d.action.kind)?n(d.questions.topic):[];return{tag:Zt(d.key),kind:d.action.kind,title:`${d.ref}: ${d.action.label}`,body:g[0]?.question||d.title,url:g.length?d.action.url:`#item=${encodeURIComponent(d.key)}`,now:!s.includes(d.action.kind),questions:g}}),r=e.needs.filter(d=>d.topic).map(d=>{const g=n(d.topic);return{tag:`topic-${d.topic}`,kind:C.questions,title:d.text,body:g[0]?.question||d.title,url:d.url,now:!0,questions:g}});return[...o,...r]}u(gn,"ownerActions");function mn(e,n){const s=e.owner.find(d=>d.id===n),o=/^item:([^/:]+\/[^/]+)$/.exec(s?.topic||"")?.[1],r=o&&e.items.find(d=>d.key===o);return!r||r.kind!=="work"||r.lane==="done"||r.held||r.blockedBy.length||r.action||r.unpicked||r.pr||r.pushed||![null,"planned","waiting"].includes(r.status)||r.agents.some(d=>!d.done)||e.owner.some(d=>d.topic===s.topic&&d.state==="open")?null:o}u(mn,"startedByAnswer");export{C as ACTION_KIND,me as BUILDER_KIND,it as BUILDER_STALLED_AFTER,dt as BUILD_REFUSED,G as BUTTON,Ne as ITEM_EVENTS,ln as ITEM_STATUSES,_t as NO_NEXT,Ct as NO_TAKE,un as buildBoard,te as builds,Ot as mergeBlockers,Tt as modelName,cn as needsAnswer,Et as noTakePicked,dn as owesNext,gn as ownerActions,jt as ownerQuestion,ee as plans,Qe as providerOf,pn as pullOf,Zt as pushTag,Be as refOf,we as roundOf,Pt as roundSettledBy,mn as startedByAnswer,Ve as threadOf,fn as todoOf,ct as waveFold};
@@ -0,0 +1 @@
1
+ var n=Object.defineProperty;var r=(e,t)=>n(e,"name",{value:t,configurable:!0});import d from"node:os";const f=r(()=>Object.values(d.networkInterfaces()).flat().map(e=>e.address),"ownAddresses"),a=r(e=>String(e||"").trim().replace(/^::ffff:/,""),"bare");function l(e,t=f()){const s=e.headers["x-forwarded-for"],o=s?String(s).split(",").at(-1):e.socket.remoteAddress;return t.includes(a(o))}r(l,"fromHost");export{l as fromHost};
@@ -0,0 +1,75 @@
1
+ // The header's bulb and its review sheet: the board's last architecture review and Run, which confirms and asks the
2
+ // board server to start one (docs/adr/0125-the-bulb-runs-an-architecture-review-of-the-boards-repo.md). The bulb glows
3
+ // while a review runs. Needs app-core.js.
4
+ BoardView.part((B) => {
5
+ const { $, state } = B;
6
+ const { esc, LINK_ARROW } = BoardUI;
7
+ const bulb = $('#archBulb');
8
+ const sheet = $('#archSheet');
9
+ const day = (iso) => new Date(iso).toLocaleDateString([], { month: 'short', day: 'numeric' });
10
+ let starting = false;
11
+
12
+ function body(a) {
13
+ const last = a.last;
14
+ if (last?.running) {
15
+ return `<p>Reviewing ${esc(a.repo)} since ${esc(BoardUI.time(last.at))}. The bulb glows until its wave is ready.</p>`
16
+ + `<button type="button" class="arch-run" data-arch-open="${esc(last.key)}">Open ${esc(last.ref)}</button>`;
17
+ }
18
+ const lastHtml = last ? `<div class="arch-last"><span>Last review: <b>${esc(last.wave)}</b> · ${esc(day(last.at))}</span>`
19
+ + `<span class="muted">${B.plural(last.findings, 'finding')}, ${last.built} built · <a href="#item=${encodeURIComponent(last.key)}" data-arch-open="${esc(last.key)}">${esc(last.ref)} ${LINK_ARROW}</a></span></div>` : '';
20
+ const waits = a.waits ? `<p class="arch-waits" role="status">${esc(a.waits)}</p>` : '';
21
+ return `${lastHtml}${waits}<button type="button" class="primary arch-run" data-arch-run${a.waits || starting ? ' disabled' : ''}>${starting ? 'Starting…' : `Run a review of ${esc(a.repo)}`}</button>`
22
+ + '<p class="muted">Claude reads the repo and makes a wave. Its first card asks you one question per finding.</p>';
23
+ }
24
+
25
+ function renderArch(d) {
26
+ const a = d.arch;
27
+ bulb.hidden = !a;
28
+ const running = Boolean(a?.last?.running);
29
+ bulb.classList.toggle('running', running);
30
+ const label = running ? 'Architecture review running' : 'Architecture review';
31
+ bulb.setAttribute('aria-label', label);
32
+ bulb.title = label;
33
+ if (sheet.open && a) $('#archBody').innerHTML = body(a);
34
+ if (sheet.open && !a) sheet.close();
35
+ }
36
+
37
+ function openCard(key) {
38
+ sheet.close();
39
+ const hash = `#item=${encodeURIComponent(key)}`;
40
+ if (location.hash === hash) window.dispatchEvent(new HashChangeEvent('hashchange'));
41
+ else location.hash = hash;
42
+ }
43
+
44
+ async function run() {
45
+ const a = state.board?.arch;
46
+ if (!a || a.waits || starting) return;
47
+ if (a.last?.open) { openCard(a.last.key); B.toast(`${a.last.ref} is this board’s open review.`); return; }
48
+ sheet.close();
49
+ if (!(await B.confirm(`Claude reads ${a.repo} and makes a wave whose first card asks you one question per finding. Run the architecture review?`)) || !B.alive()) return;
50
+ starting = true;
51
+ try {
52
+ const r = await fetch(B.site.url('api/arch-review'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' });
53
+ const answer = await r.json().catch(() => ({}));
54
+ if (!r.ok) { B.notice(answer.error || 'Could not start the review. Is the host awake?'); return; }
55
+ if (answer.existing) { openCard(answer.key); B.toast(`${answer.ref} is this board’s open review.`); return; }
56
+ await B.loadBoard();
57
+ } catch { B.notice('Could not start the review. Is the host awake?'); }
58
+ finally { starting = false; if (state.board) renderArch(state.board); }
59
+ }
60
+
61
+ B.on(bulb, 'click', () => {
62
+ const a = state.board?.arch;
63
+ if (!a || sheet.open) return;
64
+ $('#archBody').innerHTML = body(a);
65
+ sheet.showModal();
66
+ });
67
+ B.on(sheet, 'click', (ev) => {
68
+ if (ev.target === sheet || ev.target.closest('#archClose')) { sheet.close(); return; }
69
+ const open = ev.target.closest('[data-arch-open]');
70
+ if (open) { ev.preventDefault(); openCard(open.dataset.archOpen); return; }
71
+ if (ev.target.closest('[data-arch-run]')) run();
72
+ });
73
+
74
+ Object.assign(B, { renderArch });
75
+ });
@@ -70,23 +70,31 @@ BoardView.part((B) => {
70
70
  if ((!text && !files.length) || BoardAttach.busy('ask', B.site.root)) return;
71
71
  $('#askStatus').textContent = 'Sending…';
72
72
  try {
73
- await sendDraft('draft:ask', $('#askText'), () => $('#askText'), () => B.site.send('ask', { text, ...(files.length ? { files } : {}) }));
73
+ await sendDraft('draft:ask', $('#askText'), () => $('#askText'), () => B.send('ask', { text, ...(files.length ? { files } : {}) }));
74
74
  BoardAttach.clear('ask', B.site.root);
75
75
  $('#askStatus').textContent = '';
76
76
  closeSheet();
77
77
  toast('Sent');
78
- B.loadBoard();
79
78
  } catch { $('#askStatus').textContent = 'Could not send'; }
80
79
  });
81
80
 
82
- B.on(B.root, 'click', async (ev) => {
81
+ // A bump moves the New card to the top at once, its icon filling as it goes
82
+ // (docs/adr/0151-every-owner-tap-the-server-can-apply-draws-at-once.md).
83
+ async function bumpAsk(id) {
84
+ const icon = `button[data-bump="${CSS.escape(id)}"]`;
85
+ const at = new Date().toISOString();
86
+ const sending = B.send('ask-bump', { id }, B.drawItem(`ask:${id}`, (it) => ({ ...it, bumpedAt: at, ask: { ...it.ask, bumpedAt: at } })));
87
+ B.fadeBump(icon, true);
88
+ try { await sending; toast('Bumped to the top'); } catch { B.fadeBump(icon, false); toast(B.FAILED); }
89
+ }
90
+ B.on(B.root, 'click', (ev) => {
83
91
  const bump = ev.target.closest('button[data-bump]');
84
92
  if (!bump) return;
85
93
  ev.preventDefault();
86
94
  bump.disabled = true;
87
- try { await B.site.send('ask-bump', { id: bump.dataset.bump }); toast('Bumped to the top'); } catch { toast('Could not bump'); }
88
- B.loadBoard();
95
+ bumpAsk(bump.dataset.bump);
89
96
  });
97
+ B.bumpAsk = bumpAsk;
90
98
  // A New card's icons sit on the card; a message in a thread, an ask's included, carries its thread's key.
91
99
  async function pendingMessage(b, id) {
92
100
  const card = b.closest('details.item');
@@ -99,7 +107,10 @@ BoardView.part((B) => {
99
107
  }
100
108
  return { thread, key, msg, data: { id, ...(key ? { key } : {}) } };
101
109
  }
102
- // An edit box lives in state.edits and its thread draws it (ui-threads.js threadMessages), so a repaint keeps it.
110
+ // An edit box lives in state.edits and its thread draws it (ui-threads.js threadMessages), so a repaint keeps it, and
111
+ // so does leaving the board and opening it again in this page.
112
+ for (const [id, e] of B.place?.edits || []) state.edits.set(id, e);
113
+ B.keep(() => ({ edits: [...state.edits] }));
103
114
  function closeEditor(id) {
104
115
  const e = state.edits.get(id);
105
116
  state.edits.delete(id);
@@ -135,11 +146,11 @@ BoardView.part((B) => {
135
146
  B.render();
136
147
  if (msg) msg.hidden = true;
137
148
  try {
138
- await B.site.send('ask-withdrawn', { id, ...(key ? { key } : {}) });
149
+ await B.send('ask-withdrawn', { id, ...(key ? { key } : {}) });
139
150
  await hide(id, true);
140
151
  toast('Deleted');
141
- if (key) B.loadThread(key, { force: true }); else B.loadBoard();
142
- } catch { state.withdrawnAsks.delete(id); B.render(); if (msg) msg.hidden = false; toast('Could not delete'); }
152
+ if (key) B.loadThread(key, { force: true });
153
+ } catch { state.withdrawnAsks.delete(id); B.render(); if (msg) msg.hidden = false; toast(B.FAILED); }
143
154
  });
144
155
  B.on(B.root, 'submit', async (ev) => {
145
156
  const form = ev.target.closest('form.edit-form');
@@ -150,14 +161,22 @@ BoardView.part((B) => {
150
161
  if (!e) return;
151
162
  const text = form.querySelector('textarea').value.trim();
152
163
  if (!text) { toast('Write something, or delete the message'); return; }
153
- const btn = form.querySelector('button[type=submit]'); btn.disabled = true;
164
+ // The new text shows at once, in the thread and on the New card; a failed save puts the old text back and reopens
165
+ // the edit box with what was typed (docs/adr/0151-every-owner-tap-the-server-can-apply-draws-at-once.md).
166
+ const before = state.threadCache.get(e.thread);
167
+ if (before) state.threadCache.set(e.thread, before.map((m) => (m.eventId === id ? { ...m, text, edited: true } : m)));
168
+ closeEditor(id);
169
+ const title = text.split('\n')[0];
154
170
  try {
155
- await B.site.send('ask-edited', { ...e.data, text });
156
- closeEditor(id);
171
+ await B.send('ask-edited', { ...e.data, text }, B.drawItem(`ask:${id}`, (it) => ({ ...it, title, ask: { ...it.ask, text, edited: true } })));
157
172
  toast('Edited');
158
173
  B.loadThread(e.thread, { force: true });
159
- B.loadBoard();
160
- } catch { btn.disabled = false; toast('Could not save'); }
174
+ } catch {
175
+ if (before) state.threadCache.set(e.thread, before);
176
+ state.edits.set(id, { ...e, text });
177
+ B.repaintThread(e.thread);
178
+ toast(B.FAILED);
179
+ }
161
180
  });
162
181
  // The asks watch polls every 10 s while an ask waits for its reply, sooner than the board's own refresh.
163
182
  let asksSeen = false;