@lovek4d/kickflip 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +151 -115
- package/README.md +23 -14
- package/SKILL.md +37 -37
- package/bin/doctor.mjs +4 -4
- package/bin/install.mjs +3 -3
- package/bin/kickflip.mjs +34 -26
- package/bin/wait-events.mjs +2 -2
- package/board/arch-review.mjs +1 -1
- package/board/auto-review.mjs +2 -0
- package/board/bin/board.mjs +186 -69
- package/board/domain.mjs +8 -5
- package/board/finish.mjs +3 -0
- package/board/headless.mjs +3 -0
- package/board/on-host.mjs +1 -1
- package/board/permissions.mjs +2 -0
- package/board/public/app-arch.js +2 -2
- package/board/public/app-asks.js +57 -38
- package/board/public/app-banner.js +2 -2
- package/board/public/app-cards.js +156 -72
- package/board/public/app-core.js +2 -4
- package/board/public/app-header.js +12 -5
- package/board/public/app-lanes.js +201 -47
- package/board/public/app-needs.js +2 -1
- package/board/public/app-snapshot.js +2 -2
- package/board/public/app-threads.js +11 -14
- package/board/public/app.html +2 -1
- package/board/public/app.js +53 -83
- package/board/public/attach.js +39 -3
- package/board/public/dialogs.js +59 -14
- package/board/public/hub-kit.js +9 -6
- package/board/public/hub.css +5 -18
- package/board/public/linear-key.js +6 -4
- package/board/public/log.html +48 -0
- package/board/public/prompts.js +9 -8
- package/board/public/prototype-kit.js +6 -6
- package/board/public/prototype-switcher.js +13 -5
- package/board/public/prototype.html +1 -0
- package/board/public/router.js +8 -7
- package/board/public/rules-editor.js +3 -4
- package/board/public/shell.js +20 -6
- package/board/public/sidebar.css +21 -1
- package/board/public/sidebar.js +64 -31
- package/board/public/style.css +155 -62
- package/board/public/toasts.js +2 -2
- package/board/public/ui-cards.js +176 -71
- package/board/public/ui-enums.js +5 -4
- package/board/public/ui-header.js +10 -10
- package/board/public/ui-hub.js +4 -4
- package/board/public/ui-markdown.js +1 -1
- package/board/public/ui-order.js +9 -6
- package/board/public/ui-page-header.js +10 -7
- package/board/public/ui-patch.js +53 -0
- package/board/public/ui-phase-bars.js +17 -0
- package/board/public/ui-prompts.js +8 -8
- package/board/public/ui-questions.js +26 -23
- package/board/public/ui-sidebar.js +46 -22
- package/board/public/ui-snapshot.js +20 -9
- package/board/public/ui-threads.js +17 -11
- package/board/public/ui.js +80 -12
- package/board/public/view-frame.js +1 -1
- package/board/public/view-hub-settings.js +25 -18
- package/board/public/view-hub.js +53 -12
- package/board/public/view-notes.js +1 -1
- package/board/public/view-questions.js +101 -50
- package/board/public/view-settings.js +167 -49
- package/board/review.mjs +1 -1
- package/board/server.mjs +18 -12
- package/board/store.mjs +13 -12
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +20 -14
- package/docs/hub-session.md +4 -4
- package/hooks-plugin/hooks/codex-share-auto.mjs +1 -1
- package/hooks-plugin/hooks/watcher-stop.mjs +2 -2
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/close-board.mjs +1 -1
- package/hub/github.mjs +4 -4
- package/hub/home.mjs +1 -1
- package/hub/hub.mjs +12 -12
- package/hub/incoming.mjs +2 -0
- package/hub/linear.mjs +2 -2
- package/hub/live.mjs +2 -2
- package/hub/new-board.mjs +2 -2
- package/hub/permission-denied-hook.mjs +2 -0
- package/hub/pr-report.mjs +3 -1
- package/hub/presence.mjs +1 -1
- package/hub/prompts.mjs +1 -1
- package/hub/push.mjs +2 -2
- package/hub/readiness.mjs +2 -2
- package/hub/repos.mjs +3 -3
- package/hub/restart-board.mjs +1 -1
- package/hub/schedule.mjs +1 -1
- package/hub/sessions.mjs +2 -2
- package/hub/sw.js +1 -1
- package/hub/try.mjs +8 -4
- package/hub/usage.mjs +3 -3
- package/hub/views.mjs +1 -1
- package/package.json +5 -1
- package/shared/card-prs.mjs +1 -0
- package/shared/colors.css +5 -4
- package/shared/fake-gh.mjs +59 -8
- package/shared/repos.mjs +1 -1
- package/shared/usage-gate.mjs +1 -1
- package/board/public/view-polling.js +0 -87
package/board/domain.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
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]||
|
|
3
|
-
\u2192 `)},"actionText"),
|
|
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
|
-
`)}
|
|
1
|
+
var pt=Object.defineProperty;var d=(e,n)=>pt(e,"name",{value:n,configurable:!0});import{routeRecord as A}from"../hub/routes.mjs";import ft from"node:fs";import{ticketBadge as Qe,ticketIn as gt}from"./tickets.mjs";import{reviewLinks as mt,reviewTime as kt,reviewersOf as wt}from"./review.mjs";import{remoteFromInput as ht}from"../hub/repos.mjs";import{reviewsOf as yt,reviewNeeds as St,reviewActions as bt}from"./auto-review.mjs";import{cardPRs as vt,hasPR as Ge,leastReady as Ot,prKeyOf as $t,prURL as xt}from"../shared/card-prs.mjs";import Ve from"node:vm";const _e=Ve.createContext({});for(const e of["ui-enums.js","ui-order.js"])Ve.runInContext(ft.readFileSync(new URL(`./public/${e}`,import.meta.url),"utf8"),_e);const{BUTTON:F,ACTION_KIND:I,BUILDER_KIND:$e,PR_STATE:Z}=_e.BoardUI,{laneOrder:Bt}=_e.BoardUI,At=["item-message","mark-ready","grill-more","prototype-request"],Vn=["planned","waiting","building","fixing","checking","held","merged","done","dropped"],qt=30*6e4,Et="build-refused",Ce=Object.freeze({ask:"builder-ask",answer:"builder-answer"}),It=864e5,pe=d(e=>e.lane==="done"||e.lane==="asks","closedLane"),Mt=d(e=>`wave:${e}`,"waveFold"),Dt=7*It,Xe=d((e,n)=>({id:e,url:n?.workspace?`https://linear.app/${n.workspace}/issue/${e}`:null}),"linearRef"),_t=d((e,n)=>String(e).replace(/^S(\d+)$/,`${n||"INT"}-$1`),"internalRef"),Re=d((e,n,s)=>gt(s,n.ticket)?n.ticket:e.internal===!0?_t(n.id,s?.prefix):n.id,"refOf"),Ct=d((e,n,s,o)=>Qe(e||(n?{source:"internal",prefix:"INT"}:null),s,o)||(n?{id:String(n),url:null,source:null}:null),"ticketOf");function Rt(e,n,s,o,r,a){const p=(e.status||"").toLowerCase();return p==="merged"||p==="done"||p==="dropped"||!r&&(n?.state==="MERGED"||n?.state==="CLOSED")?"done":s||p==="checking"?"progress":o?"review":n?.state==="OPEN"?n.checks?.state==="fail"&&(!n.isDraft||a)?"progress":"review":n?"progress":"todo"}d(Rt,"laneOf");function Nt(e,n){return e?.state==="MERGED"||n.status==="merged"||n.merged_at?Z.merged:e?.state==="CLOSED"?Z.closed:e?.state==="OPEN"?e.isDraft?Z.draft:Z.open:null}d(Nt,"prStateOf");const Ne=d(e=>{const n=(e.status||"").toLowerCase();return n==="dropped"||e.pr?.state==="CLOSED"&&n!=="merged"&&n!=="done"},"droppedCard");function Lt(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(Ne).length;return{merged:n.length-s,dropped:s}}d(Lt,"finishedOf");function Tt(e,n,s){return Object.fromEntries(e.map(o=>{const r=A(n?.reviews,o.github),a=r==="APPROVED"?"approved":r==="CHANGES_REQUESTED"?"changes":Object.hasOwn(s,o.name)?"sent":"unsent";return[o.name,{state:a,since:a==="sent"?A(s,o.name):null,github:o.github||null}]}))}d(Tt,"reviewOf");function Pt(e,n,s,o,r,a){return s?.kind===$e.install?"builder":o==="checking"?"checking":s?"builder":r&&e==="review"?"owner":e==="review"&&n?.state==="OPEN"&&n.isDraft?a?"ci":"ready":e==="review"||e==="stale"?n?.reviewDecision==="APPROVED"?"merge":"reviewers":null}d(Pt,"waitsOnOf");const xe=/(^|\/)prototype-[\w-]+\.html/,jt={reviewers:"Waiting for PR reviewers",merge:"Approved, merges once CI and the merge state allow",ci:"Marked ready for review once CI passes"},Ye={waiting:"Approved, Merge shows once CI and the merge state allow",ready:"Approved and green: tap Merge",merging:"Merging"},Ut="Fix the failing CI",He="Tap Done once it is settled",Wt=["status","agent","ask-link",Ce.answer],ze=d(e=>(e.who==="claude"||e.via==="github")&&!Wt.includes(e.kind),"countsUnread");function Kt({slug:e,wave:n,it:s,order:o,prs:r,repo:a,remote:p,base:u,builder:c,reachedPhase:f,agents:k,worktree:i,eta:h,thread:O,events:w,working:M,said:Q,askedAt:E,topicItems:G,boardReviewers:V,now:P,bumpedAt:l,holdAsk:$,closedAt:S,mergeAt:x,autoMerge:ee,skipDraft:z,tickets:N}){const J=r.map(m=>({repo:m.repo,number:m.number,url:xt(m),...m.gh||{},status:Nt(m.gh,s)})),j=Ot(r.map(m=>m.gh)),C=r[j]?.gh||null,K=J[j]?.status??null,T=!!S&&String(S)>String(s.status_at||"");T&&s.status!=="merged"&&(s={...s,status:"done"});const ge=[T&&S,s.status_at,s.merged_at,...r.map(m=>m.gh?.mergedAt)].filter(Boolean).map(String).sort().pop()||null,L=String(ge||""),D=["done","merged"].includes(s.status)||K===Z.merged&&s.status!=="dropped"?[...Q.filter(m=>m.kind==="item-message").map(m=>m.ts),...E].map(String).filter(m=>m>L).sort().pop():void 0,me=s.owner_step&&!T&&s.status!=="dropped"?String(s.owner_step):null,le=["done","merged"].includes(s.status)||K===Z.merged;D&&(s={...s,status:void 0,next:void 0});const ke=!!D&&Q.some(m=>String(m.ts)>L&&(m.state==="waiting"||m.state==="working")),te=`${e}/${s.id}`,we=wt(n,V),U=Object.fromEntries(Object.entries(mt(n,s,te,G)).map(([m,v])=>[m,kt(v.ts)])),X=Object.values(U).filter(Boolean).sort()[0]||null,he=[...O.map(m=>m.ts),...w.map(m=>m.ts)].sort().pop()||null,Ae=[...O.filter(m=>ze({...m,who:m.who||"claude"})).map(m=>m.ts),...w.map(m=>m.reply?.at)].filter(Boolean).sort(),W=!Ge(s)&&s.pushed?.branch?{branch:s.pushed.branch,at:s.pushed.at||null,host:p?.host||null}:null;let B=Rt(s,C,k.some(m=>m.started&&!m.done&&Y(m)),W,!!D,z);D&&B==="todo"&&(B="progress"),B==="review"&&X&&P-Date.parse(X)>Dt&&(B="stale");const ne=!!me&&le&&B==="done";ne&&(B="review");let R=Pt(B,C,c,(s.status||"").toLowerCase(),W,z);const ie=R==="builder"&&C?.state==="OPEN"&&!C.isDraft,de=B==="progress"&&!R&&C?.state==="OPEN"&&(!C.isDraft||z),se=ne||!!D&&B==="progress"&&!R&&!de&&!ke;se&&(R="owner");const ue=$&&String($.ts)>String(s.held_at||""),ye=B==="todo"&&(ue?!!$.data.hold:s.status==="held"||!!s.held),qe=ye&&(ue?$.data.until:s.pick_up_at)||null,oe=B==="todo"||B==="progress",Se=k.filter(Y).map(m=>String(m.started||"")).filter(Boolean).sort()[0]||"",Ee=l&&!(Se&&String(l)<Se),be=O.filter(m=>(m.who||"claude")==="claude"&&m.kind!=="status").map(m=>String(m.ts)).sort().pop()||"",ce=R==="merge"&&!!x&&String(x)>be,ve=O.filter(m=>m.kind!=="status").sort((m,v)=>String(m.ts).localeCompare(String(v.ts))).at(-1),Ie=B==="todo"&&ve?.kind===Et?ve.text:null,t=D||k.filter(Y).map(m=>m.started).filter(Boolean).map(String).sort().pop()||s.queued_at||["building","fixing","checking"].includes(String(s.status||"").toLowerCase())&&s.status_at||null,g=W&&R==="owner"?`merge ${W.branch} into ${u}`:R==="merge"&&!ee?Ye[ce?"merging":"waiting"]:jt[R],y=K===Z.draft?`Review draft PR #${J[j].number}`:null;return{key:te,kind:"work",order:o,wave:e,waveTitle:n.title||e,id:s.id,ref:Re(n,s,N),title:s.title,status:s.status||null,created:s.created||null,progressSince:t,closedAt:B==="done"?ge:null,internal:n.internal===!0,repo:n.internal===!0&&!n.repo?null:a,lane:B,waitsOn:R,builder:R==="checking"?null:c,reachedPhase:f,agents:k,worktree:i??null,eta:h,working:M,reopened:!!D,awaitsDone:se,ownerStep:se?me:null,queuedAt:B!=="done"&&s.queued_at||null,queued:!1,deps:s.deps||[],held:ye,heldUntil:qe,risky:!!s.risky,bumpable:oe,bumpedAt:oe&&Ee?l:null,holdable:B!=="done",hasNotes:!!(s.plan||String(s.notes||"").trim()),finishable:B!=="done",merging:ce,refusal:Ie,manualSteps:s.manual_steps||[],fromAsk:s.from_ask||null,next:ie||B==="done"||R==="checking"?null:de?Ut:g||s.next||y||null,branch:s.branch||C?.headRefName||W?.branch||null,pushed:B==="done"||ne?null:W,ticket:N?.source==="linear"&&!s.ticket&&s.linear?.length?null:Ct(N,s.ticket,s.ticket||(n.internal?Re(n,s,N):s.linear?.[0]),{repo:a,key:te}),ticketSource:N?.source||null,linear:(s.linear||[]).map(m=>N?.source==="linear"&&Qe(N,m,{key:te,repo:a})||Xe(m,N)),pr:J[j]||null,prs:J,reviewers:we.map(m=>m.name),review:Tt(we,C,U),inReviewSince:X,threadCount:O.length+w.length,lastTs:he,othersAt:Ae,grilling:nt("grill",w,k,E),prototyping:nt("prototype",w,k,E),state:s.state||null,updatedAt:C?.mergedAt||C?.updatedAt||s.merged_at||c?.at||he||null,ask:null,fold:null,action:null,install:B==="done"?null:Ft(s.install,C)}}d(Kt,"workItem");function Ft(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))}}d(Ft,"installOf");function Qt(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)}d(Qt,"mergeBlockers");const Gt=d(e=>!Qt(e).length,"mergeable");function Vt(e,n,s){if(e.held||pe(e))return null;const o=`#item=${encodeURIComponent(e.key)}`;if(Je(e)){const r=n.get(e.questions.first);return xe.test(r?.url||"")?{kind:I.pick,url:`questions.html#${r.id}`,label:"Prototypes ready: pick a take"}:{kind:I.questions,url:`questions.html#${e.questions.first}`,label:`Answer ${H(e.questions.open,"question")}`}}return e.waitsOn==="ready"?{kind:I.ready,url:e.pr.url,label:`Review draft PR #${e.pr.number}`}:e.kind==="ask"&&e.waitsOn==="owner"?{kind:I.done,url:o,label:e.next||He}:e.waitsOn==="merge"&&e.manualSteps.length?{kind:I.steps,url:o,label:"Run the manual steps, then merge"}:e.waitsOn==="merge"&&!s&&!e.merging&&e.prs.every(r=>["MERGED","CLOSED"].includes(r.state)||Gt(r))?{kind:I.merge,url:o,label:"Merge"}:null}d(Vt,"actionOf");const Je=d(e=>!!(e.questions&&e.questions.state!=="resolved"&&e.questions.open>0),"asking"),Ze=new Set([I.pick,I.questions]);function et(e){return pe(e)?null:Je(e)?F.openQuestions:e.action?.kind===I.done?F.markDone:e.kind==="ask"?null:F.grill}d(et,"actionButtonOf");const Xt=d(e=>e.kind==="work"&&e.lane==="todo"&&!e.action&&!e.unpicked,"buildableOf"),Yt=d((e,n)=>/^item:/.test(e.topic||"")&&xe.test(e.url||"")&&!(e.options||[]).some(s=>s.id===n),"picksNoTake");function Ht(e,n){const s=e.filter(a=>a.topic===`item:${n}`),r=[...s.filter(a=>xe.test(a.url||"")).flatMap(a=>a.answers.map(p=>({q:a,a:p,again:Yt(a,p.choice)}))),...s.filter(a=>a.request).map(a=>({q:a,a:a.request,again:!0}))].sort((a,p)=>String(a.a.ts).localeCompare(String(p.a.ts))).at(-1);return r?.again?{id:r.q.id,note:r.a.note.replace(/\s+/g," ").trim()}:null}d(Ht,"noTakePicked");const zt="Prototype again",Jt=Object.freeze({[I.pick]:F.openQuestions,[I.questions]:F.openQuestions,[I.ready]:F.github,[I.steps]:F.github,[I.merge]:F.merge,[I.done]:F.markDone});function Zt(e,n,s){if(e.action=Vt(e,n,s),e.action?.kind===I.questions&&e.questions.fromAsks){Object.assign(e,{action:null,waitsOn:"owner",next:e.action.label,actionButton:et(e),resolvedBy:null});return}e.awaitsDone&&=!e.action,e.awaitsDone||(e.ownerStep=null),e.actionButton=et(e),e.resolvedBy=Jt[e.action?.kind]||null,e.action?.kind===I.merge&&(e.next=Ye.ready),e.awaitsDone&&Object.assign(e,{lane:"review",next:e.ownerStep||He,bumpable:!1,bumpedAt:null});const o=e.lane==="todo"||e.waitsOn==="ready"||e.lane==="progress"&&e.waitsOn==="owner";if(!e.action||!o)return;const r=e.lane==="todo"&&Ze.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})}d(Zt,"placeOwnerWait");const tt=d(e=>e.queuedAt||e.agents.some(Y)||["building","fixing"].includes(String(e.status||"").toLowerCase()),"started"),en="none set yet";function Xn(e,n){const s=String(e.status||"").toLowerCase();if(e.next||Ge(e)||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)&&tt({status:s,agents:o})}d(Xn,"owesNext");function tn(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}`}d(tn,"askedLabel");function nn(e,n,s,o,r,a,p){const u=n.find(S=>S.kind==="ask"&&S.eventId===e.id),c=u.files,f=(u.text||"").trim().split(`
|
|
2
|
+
`)[0]||c.map(S=>S.name).join(", ")||"Ask",k=u.state==="replied"||u.state==="answered",i=r?.state==="progress"&&!(String(a||"")>String(r.at||"")),h=!i&&(!!r||!!a),O=k&&!h&&!i&&p.open>0,w=k&&!h&&!i&&!O&&p.answered>0,M=h||k&&!i&&!O&&!w,Q=k&&!o&&!r?.moved&&[...n.filter(S=>S.who==="claude"&&S.kind!=="ask-link"&&S.kind!=="status").map(S=>S.ts),...n.map(S=>S.reply?.at)].filter(Boolean).sort().pop()||null,E=M||i||O||w,G=n.some(S=>S.who==="love"&&S.kind!=="ask"&&(S.state==="waiting"||S.state==="working")),V=E?"answered":u.state,P=E?null:s||null,l=[...n.filter(ze).map(S=>S.ts),...n.map(S=>S.reply?.at)].filter(Boolean).sort(),$=[...n.map(S=>S.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:E?tn(e.ts):"New",isNew:!E,title:f,status:null,internal:!1,repo:null,lane:i?"progress":M?"asks":"todo",waitsOn:i&&!G?"owner":null,builder:null,agents:[],eta:null,working:n.some(S=>S.state==="working"),deps:[],held:!1,risky:!1,bumpable:!E,bumpedAt:P,manualSteps:[],fromAsk:null,from:null,next:i&&r.next||null,hasNotes:!1,branch:null,finishable:E&&!M,linear:[],pr:null,prs:[],reviewers:[],review:{},inReviewSince:null,progressSince:i&&r.at||null,closedAt:null,threadCount:n.length,lastTs:$,othersAt:E?l:[],grilling:!1,prototyping:!1,created:e.ts,updatedAt:E?$:e.ts,ask:{eventId:e.id,state:V,askedAt:e.ts,bumpedAt:P,edited:!!u.edited,canEdit:!E,text:u.text,files:c,answerAt:Q,from:u.from||null},fold:null,action:null}}d(nn,"askItem");const sn=new Set(["grill","prototype"]),re=d(e=>sn.has(e.kind),"plans"),Y=d(e=>!re(e),"builds"),on={grill:"preheating",prototype:"prototyping"},rn={grill:"grill-more",prototype:"prototype-request"},Le=d((e,n)=>e.kind===rn[n]&&!e.data?.question,"isRequest");function nt(e,n,s,o){if(s.some(a=>a.kind===e&&a.started&&!a.done))return!0;const r=n.filter(a=>Le(a,e)).pop();return!!(r&&!r.reply&&!o.some(a=>String(a)>String(r.ts))&&!s.some(a=>a.kind===e&&a.done&&String(a.started)>String(r.ts)))}d(nt,"planningOf");function Te(e={}){return Object.entries(e).filter(([,n])=>n&&typeof n=="object").map(([n,s])=>({name:n,kind:st(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,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))}d(Te,"agentsOf");function an(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}d(an,"worktreeOf");function ln(e,n=[]){const s=e.filter(Y).map(o=>String(o.started||"")).filter(Boolean).sort().pop();return s===void 0?null:n.filter(o=>o.kind==="status"&&o.phase&&!o.planner&&String(o.ts)>=s).sort((o,r)=>String(o.ts).localeCompare(String(r.ts))).pop()?.phase||null}d(ln,"reachedPhaseOf");function dn(e,n=[],s){const o=e.filter(i=>i.started&&!i.done),r=o.every(re),a=r?o:o.filter(Y),p=a.map(i=>String(i.started||"")).sort()[0];if(p===void 0)return null;const u=n.filter(i=>i.kind==="status"&&String(i.ts)>=p&&(r||!i.planner)).sort((i,h)=>String(i.ts).localeCompare(String(h.ts))).pop(),c=a.some(i=>i.kind===$e.install)?$e.install:st(a.at(-1).kind),f=u?.ts||p;return{progress:u?u.phase||u.planner?u.text.replace(": "," \xB7 "):u.text||null:r?on[c]:null,phase:u?.phase||null,at:f,kind:c,planner:r,silentSince:f,stalled:s-Date.parse(f)>=qt}}d(dn,"builderOf");const Pe={opus:"Claude Opus",sonnet:"Claude Sonnet",codex:"Codex"},un=d(({model:e,kind:n})=>Pe[e]||Pe[n]||(n==="claude"?Pe.opus:null),"modelName"),st=d(e=>e==="opus"||e==="sonnet"?"claude":e,"providerOf");function je(e){const n=un(e),{kind:s}=e,o=e.label?.replace(/\bbuilder\b/,"implementer").replace(/\bBuilder\b/,"Implementer"),r=d(p=>p[0].toLowerCase()+p.slice(1),"lower");if(n&&re(e))return`${n} ${s}`;if(n)return`${n} ${r(o?.replace(/^(?:claude\s+)?(?:opus|sonnet|codex)\b\s*/i,"")||"implementer")}`;const a=s?s[0].toUpperCase()+s.slice(1):"";return a?o?o.toLowerCase().includes(s.toLowerCase())?o:`${a} ${r(o)}`:a:o||"Implementer"}d(je,"agentName");const ot=d((e,n)=>!!e.resolved&&(!e.resolved_at||String(e.resolved_at)>=String(n)),"resolvedSince");function cn(e,n,s=[]){const o=n.filter(f=>f.data?.id===e.id),r=o[o.length-1]||null,a=!!(e.resolved&&e.resolved_at&&r&&String(r.ts)>e.resolved_at),p=e.resolved&&!a,u=s.filter(f=>f.data?.question===e.id&&String(f.ts)>String(r?.ts||"")).at(-1),c=p?"resolved":r?r.reply?"replied":"answered":"open";return{...e,state:c,request:u?{id:u.id,ts:u.ts,note:u.data.note||"",choice:u.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:ot(e,f.ts)}))}}d(cn,"ownerQuestion");const ae=d(e=>e.round??(xe.test(e.url||"")?1:0),"roundOf");function pn(e,n){const s=d((o,r)=>o.confirm_of||(o.options||[]).some(a=>a.id===r.choice),"settles");return e.filter(o=>ae(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)}d(pn,"roundSettledBy");function fn(e){const n=e.find(r=>r.request&&!e.some(a=>ae(a)>ae(r)));if(n)return{n:ae(n)+1,state:"making"};const s=Math.max(0,...e.map(ae));if(!s)return null;const o=pn(e,s);return o?{n:s,state:"closed",take:o.choice}:e.some(r=>ae(r)===s&&r.state!=="resolved")?{n:s,state:"back"}:null}d(fn,"roundState");const fe=d((e,n,s)=>{const o=e.get(n);o?o.push(s):e.set(n,[s])},"add"),Be=d((e,n)=>{const s=new Map;for(const o of e)fe(s,n(o),o);return s},"groupBy");function gn(e,n,s=new Set){const o=new Set(e.map(p=>p.id)),r=[...e];n.some(p=>!o.has(p.topic))&&r.push({id:"",title:"Other"});const a=Be(n,p=>o.has(p.topic)?p.topic:"");return r.map(p=>{const u=a.get(p.id)||[],c=d(i=>u.filter(h=>h.state===i).length,"count"),f=u.every(i=>i.state==="resolved")||s.has(p.id)&&!c("open"),k=u.length&&f?"resolved":"open";return{...p,state:k,questions:u.map(i=>i.id),open:c("open"),answered:c("answered"),replied:c("replied"),resolved:c("resolved"),round:fn(u)}}).filter(p=>p.questions.length)}d(gn,"ownerTopics");const mn={"mark-ready":"Mark ready","grill-more":"grill requested","prototype-request":"prototype requested"},kn="quick build: no grill, as you asked",wn=d((e,n)=>{if(e.kind!=="prototype-request"||!n.has(e.data.question))return e.data.text||A(mn,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"),Ue=d(e=>[...e.filter(n=>n.who==="claude"&&!["status","agent",Ce.answer].includes(n.kind)).map(n=>n.ts),...e.filter(n=>n.kind==="ask").map(n=>n.reply?.at)].filter(Boolean).map(String).sort().pop()||"","claudeAtOf");function rt(e,n={}){const s=new Map,o=new Map;for(const k of n.items||[])fe(s,k.topic,k),o.set(k.id,k.topic);const r=new Map,a=new Map,p=new Map,u=new Map,c=new Map,f=new Map;e.forEach((k,i)=>{f.set(k,i),k.data?.key&&At.includes(k.kind)&&fe(r,k.data.key,k),k.kind==="owner-answer"&&o.has(k.data?.id)&&fe(a,o.get(k.data.id),k),k.kind==="ask"&&p.set(k.id,k)});for(const k of e.filter(i=>i.kind==="ask-edited").sort((i,h)=>String(i.ts).localeCompare(String(h.ts)))){u.set(k.data?.id,k.data?.text);for(const i of k.data?.files||[])fe(c,k.data?.id,i)}return{byKey:r,answers:a,asks:p,edits:u,editFiles:c,questions:s,order:f}}d(rt,"threadIndex");const We=d((e,n)=>{const s=n.filter(o=>o?.length);return s.length>1?s.flat().sort((o,r)=>e.order.get(o)-e.order.get(r)):[...s[0]||[]]},"inLogOrder"),hn=/; its log is \S*\/\.claude\/kickflip\/logs\/([^/\s]+\.log)\.$/,yn=d(e=>{const n=!e.log&&typeof e.text=="string"?hn.exec(e.text):null;return n?{...e,text:`${e.text.slice(0,n.index)}.`,log:n[1]}:e},"withLog");function at(e,{lines:n=[],events:s=[],owner:o={},working:r={},fromAsk:a=null,links:p=[],agents:u={},moved:c=[],index:f=rt(s,o)}){const k=e.startsWith("ask:")?[e.slice(4)]:[a,...c].filter(Boolean),i=[...new Set([e,...k.map(l=>`ask:${l}`)])],h=new Map((f.questions.get(`item:${e}`)||[]).map(l=>[l.id,l])),O=f.answers.get(`item:${e}`)||[],w=new Set([...h.values()].map(l=>l.round_of).filter(Boolean)),M=Te(u).filter(Y).map(l=>String(l.started||"")).filter(Boolean),Q=new Set(O.filter(l=>ot(h.get(l.data.id),l.ts)||w.has(l.data.id)||M.some($=>$>String(l.ts))).map(l=>l.id)),E=[...Te(u).flatMap(l=>[...l.started?[{ts:l.started,who:"claude",kind:"agent",text:`${je(l)} started`,finished:!1,eta:l.done?null:l.eta,...re(l)?{planner:!0}:{}}]:[],...l.done?[{ts:l.done,who:"claude",kind:"agent",text:`${je(l)} finished`,finished:!0,eta:null,...re(l)?{planner:!0}:{}}]:[],...l.calls.length?[{ts:l.done||l.calls.at(-1).at,who:"claude",kind:"calls",text:je(l),calls:l.calls.map($=>$.text),verified:l.verified}]:[]]),...n.map(yn).map(l=>({ts:l.ts,who:l.who||"claude",...l.log?{log:l.log}:{},...l.kind==="status"?{kind:"status",...l.planner?{planner:!0}:{}}:{},...Object.values(Ce).includes(l.kind)?{kind:l.kind,ask:l.ask}:{},text:l.text||"",html:l.html||null,link:l.link||null,via:l.via||null})),...We(f,i.map(l=>f.byKey.get(l))).map(l=>({ts:l.ts,who:"love",via:"site",kind:l.kind,text:wn(l,h),files:l.data.files||[],eventId:l.id,reply:l.reply,...Le(l,"grill")||Le(l,"prototype")?{request:!0}:{}})),...O.map(l=>{const $=[l.data.label||l.data.choice,l.data.note].filter(Boolean).join(" \xB7 ");return{ts:l.ts,who:"love",via:"questions",kind:l.kind,text:`${h.get(l.data.id).question}
|
|
4
|
+
\u2192 ${$}`,files:l.data.files||[],eventId:l.id,reply:l.reply}}),...We(f,[...new Set(k)].map(l=>f.asks.has(l)?[f.asks.get(l)]:[])).flatMap(l=>[{ts:l.ts,who:"love",via:"ask",kind:l.kind,text:l.data?.text||"",files:l.data?.files||[],eventId:l.id,reply:l.reply,...c.includes(l.id)?{moved:!0}:{},...typeof l.data?.from?.title=="string"?{from:l.data.from.title}:{}},...l.data?.quick===!0?[{ts:l.ts,who:"love",kind:"quick-build",request:!0,text:kn}]:[],...p.map($=>({ts:l.ts,who:"claude",kind:"ask-link",text:`${$.became?"Became":"Added to"} ${$.ref}`,link:`#item=${encodeURIComponent($.key)}`,html:null,via:null}))])].sort((l,$)=>String(l.ts).localeCompare(String($.ts))),G=Ue(E),{edits:V,editFiles:P}=f;for(const l of E){if(!l.eventId)continue;(l.kind==="ask"||l.kind==="item-message")&&typeof V.get(l.eventId)=="string"&&Object.assign(l,{text:V.get(l.eventId),edited:!0,files:[...l.files,...P.get(l.eventId)||[]]});const $=l.kind==="ask"&&(p.length>0||!!l.moved);l.state=l.reply?"replied":$||Q.has(l.eventId)||G>String(l.ts)?"answered":A(r,l.eventId)?"working":"waiting",l.canDelete=l.kind==="item-message"&&(l.state==="working"||l.state==="waiting"),l.canEdit=l.canDelete}return E}d(at,"threadOf");function Sn(e,n,s,o,r,a){const p=d((i,h)=>{const O=a.filter(M=>M.kind==="wave-triage"&&M.data?.[i]===h).pop(),w=r.filter(M=>M.topic===`${i}:${h}`&&(M.state==="open"||M.state==="answered")).length;return{running:!!(O&&!O.reply),questions:w}},"triageOf"),u=new Map(o.map(i=>[i.key,i])),c=d(i=>i.filter(h=>h.lane==="done"&&!Ne(h)).length,"merged"),f=s.map(i=>({l:i,cards:i.keys.map(h=>u.get(h)).filter(Boolean)})).map(i=>({...i,open:!i.l.finished_at&&i.cards.some(h=>h.lane!=="done")})).sort((i,h)=>Number(i.open)-Number(h.open));for(const{l:i,cards:h}of f)for(const O of h)O.lineup={slug:i.slug,title:i.title};const k=f.filter(i=>i.open).map(({l:i,cards:h})=>({kind:"lineup",slug:i.slug,title:i.title,goal:i.goal,keys:h.map(O=>O.key),phases:i.phases.map(O=>O.filter(w=>u.has(w))).filter(O=>O.length),next:bn(h),merged:c(h),total:h.length,triage:p("lineup",i.slug)}));return e.fromWaves?[...k,...n.filter(i=>!i.finished).map(i=>({w:i,cards:o.filter(h=>h.wave===i.slug&&h.kind!=="ask")})).filter(({cards:i})=>i.length).map(({w:i,cards:h})=>({kind:"wave",slug:i.slug,title:i.title,goal:null,keys:h.map(O=>O.key),phases:null,next:null,merged:c(h),total:h.length,triage:p("wave",i.slug)}))]:k}d(Sn,"waveBandsOf");function bn(e){const n=e.filter(a=>a.lane==="todo"),s=n.find(a=>a.ready&&a.buildable)||n[0];if(!s)return null;const o=s.ready&&s.buildable,r=o?"ready":s.held?"held":s.blockedBy.length?`waits on ${s.blockedBy.map(a=>a.ref).join(", ")}`:s.action?"needs you":s.unsorted?"new":s.state||"not ready";return{key:s.key,ref:s.ref,ready:o,word:r}}d(bn,"nextOf");function Yn(e,n,s,o){const r=e.waveBands.find(i=>i.kind==="lineup"&&i.slug===n);if(!r)return null;const a=new Map(e.items.map(i=>[i.key,i.lane])),p=r.keys.filter(i=>a.get(i)==="todo"),u=p.indexOf(s),c=u+o;if(u<0||c<0||c>=p.length)return null;[p[u],p[c]]=[p[c],p[u]];let f=0;const k=r.keys.map(i=>a.get(i)==="todo"?p[f++]:i);return f=0,r.phases.map(i=>k.slice(f,f+=i.length))}d(Yn,"lineupMoved");const vn=d(e=>e.phases||e.items.map(n=>[n]),"lineupPhases");function Hn(e){const{owner:n={},topicItems:s={},events:o=[],threads:r={},prs:a={},working:p={}}=e,u=e.now??Date.now(),c=rt(o,n),f=new Set(o.filter(t=>t.kind==="ask-withdrawn"&&!t.data?.key).map(t=>t.data?.id)),k=new Map(o.filter(t=>t.kind==="ask"&&!f.has(t.id)).map(t=>[t.id,t])),i=d((t,g)=>new Map(o.filter(y=>y.kind===t).sort((y,m)=>String(y.ts).localeCompare(String(m.ts))).map(y=>[y.data?.[g],y.ts])),"latest"),h=i("ask-bump","id"),O=i("card-unbump","key"),w=new Map([...i("card-bump","key")].filter(([t,g])=>!(String(O.get(t)||"")>String(g)))),M=i("card-done","key"),Q=i("card-merge","key"),E=e.autoMerge===!0,G=e.tickets,V=new Map(o.filter(t=>t.kind==="card-hold").sort((t,g)=>String(t.ts).localeCompare(String(g.ts))).map(t=>[t.data?.key,t])),P=new Map,l=e.askLinks||{},$=new Map;for(const[t,g]of Object.entries(l))k.has(t)&&$.set(g,[...$.get(g)||[],t]);const S=[],x=[],ee=new Map,z=new Map,N=new Map,J=new Map,j=e.github?null:ht(e.remote);for(const{slug:t,wave:g,agents:y={}}of e.waves){const m=e.github||g.repo||null;S.push({slug:t,title:g.title||t,status:g.status||null,internal:!!g.internal,finished:null,reviewers:(g.reviewers||[]).map(v=>v.name)}),g.items.forEach((v,q)=>{const _=`${t}/${v.id}`,Ke=($.get(_)||[]).filter(b=>b!==v.from_ask),Me=[v.from_ask,...Ke].filter(b=>k.has(b)),ut=new Set(Me.map(b=>`ask:${b}`)),Fe=[...A(r,_)||[],...Me.flatMap(b=>A(r,`ask:${b}`)||[])];v.from_ask&&P.set(v.from_ask,[...P.get(v.from_ask)||[],{key:_,ref:Re(g,v,G),became:!0}]);const Oe=at(_,{lines:Fe,events:o,owner:n,working:p,fromAsk:v.from_ask||null,moved:Ke,index:c});for(const b of Oe)b.eventId&&ee.set(b.eventId,b.state);z.set(_,Ue(Oe)),x.push(Kt({slug:t,wave:g,it:v,order:q,repo:m,remote:j,base:e.branch||"main",prs:vt(v,m).map(b=>({...b,gh:A(a,$t(b))||null})),...(b=>({agents:b,builder:dn(b,A(r,_),u),reachedPhase:ln(b,A(r,_)),eta:b.map(ct=>ct.eta).filter(Boolean).sort().pop()||null}))(Te(A(y,v.id))),worktree:an(A(y,v.id)),thread:Fe,events:[...We(c,[_,...ut].map(b=>c.byKey.get(b))),...Me.map(b=>k.get(b))],working:Oe.some(b=>b.state==="working"),said:Oe.filter(b=>b.who==="love"&&(b.kind==="item-message"||b.kind==="owner-answer")),askedAt:(c.questions.get(`item:${_}`)||[]).filter(b=>b.asked_at).map(b=>b.asked_at),topicItems:s,boardReviewers:e.reviewers||[],now:u,bumpedAt:w.get(_),holdAsk:V.get(_),closedAt:M.get(_),mergeAt:Q.get(_),autoMerge:E,skipDraft:e.skipDraft===!0,tickets:G}));const De=x[x.length-1];J.set(_,{parent:v.parent?it(t,v.parent):null,related:(v.related||[]).map(b=>it(t,b))}),De.lane==="done"&&N.set(_,De.closedAt||String(De.updatedAt||""))})}const C=new Map(x.map(t=>[t.key,t])),K=new Map,T=e.try||{},ge=e.autoBuild===!0,L=["web","app"].includes(T.kind)?T.kind:e.waves.some(({wave:t})=>t.items.some(g=>g.install?.url))?"app":"web";for(const t of x){if(t.kind!=="work"||t.lane==="done")continue;const g=A(T.cards,t.key)||{},y=g.head||t.pr?.headRefOid,m=L==="web"?g.sha:t.install?.sha,v=t.builder?.kind===$e.install?"running":g.state||(L==="app"&&t.install?"ready":null);t.try={kind:L,state:v,stale:!!(m&&y&&!String(y).toLowerCase().startsWith(String(m).toLowerCase())),url:L==="web"?null:t.install?.url||null,port:L==="web"&&v==="ready"&&g.https||null,sha:m||null,changes:g.ahead>0,script:g.script===!0,autoBuild:ge}}const D=T.main,me=D?.script&&D.sha?{slug:D.slug,branch:D.branch||"main",sha:D.sha.slice(0,7),at:D.at,kind:L,state:D.state||null,url:L==="web"?null:e.main?.install?.url||null,port:L==="web"&&D.state==="ready"&&D.https||null,pull:T.pull?{running:T.pull.running===!0,error:T.pull.error||null}:null}:null,le=e.askProgress||{},ke=i("ask-done","id"),te=Be(o.filter(t=>t.kind==="owner-answer"),t=>t.data?.id),we=Be(o.filter(t=>t.kind==="prototype-request"),t=>t.data?.question),U=(n.items||[]).map(t=>cn(t,te.get(t.id)||[],we.get(t.id)||[])),X=new Map(U.map(t=>[t.id,t])),he=Be(U,t=>t.topic),Ae=d(t=>{const g=he.get(t)||[];return{open:g.filter(y=>y.state==="open").length,answered:g.filter(y=>y.state==="answered").length}},"pendingOn");for(const t of k.values()){const g=`ask:${t.id}`,y=C.get(A(l,t.id)),m=[...P.get(t.id)||[]];if(y&&!m.some(q=>q.key===y.key)&&m.push({key:y.key,ref:y.ref,became:!1}),m.length){K.set(g,m[0].key);continue}if(A(l,t.id)){K.set(g,A(l,t.id));continue}const v=at(g,{lines:A(r,g)||[],events:o,working:p,links:m,index:c});for(const q of v)q.eventId&&ee.set(q.eventId,q.state);z.set(g,Ue(v)),x.push(nn(t,v,h.get(t.id),m.length>0||!!A(l,t.id),A(le,t.id),ke.get(t.id),Ae(`item:${g}`)))}for(const t of S){const g=x.filter(y=>y.wave===t.slug);if(t.finished=Lt(g),t.finished)for(const y of g)y.lane==="done"&&(y.fold=Mt(t.slug))}for(const t of x)t.lineup=null;const W=new Set((n.topics||[]).map(t=>t.id)),B=e.waves.flatMap(({slug:t,wave:g})=>(g.lineups||[]).map(y=>{const m=vn(y).map(v=>v.map(q=>`${t}/${q}`));return{...y,phases:m,keys:m.flat()}})),ne=d(()=>gn([...n.topics||[],...x.filter(t=>!W.has(`item:${t.key}`)).map(t=>({id:`item:${t.key}`,card:t.key,title:`${t.ref}: ${t.title}`,...t.kind==="ask"?{askedAt:t.ask.askedAt}:{},summary:"Questions about this work item."})),...B.filter(t=>!W.has(`lineup:${t.slug}`)).map(t=>({id:`lineup:${t.slug}`,title:t.title,summary:"Wave triage."})),...(e.fromWaves?S:[]).filter(t=>!W.has(`wave:${t.slug}`)).map(t=>({id:`wave:${t.slug}`,title:t.title,summary:"Wave triage."}))],U,new Set(x.filter(pe).map(t=>`item:${t.key}`))),"topicsOf"),R=new Map(ne().map(t=>[t.id,t]));for(const t of x){const g=R.get(`item:${t.key}`);if(!g){t.questions=null;continue}const y=g.questions.filter(q=>X.get(q)?.state!=="resolved"),m=g.questions.find(q=>X.get(q)?.state==="open")||y[0]||g.questions[0],v=g.questions.map(q=>X.get(q)).filter(q=>q?.state==="open");t.questions={topic:g.id,state:g.state,open:g.open,answered:g.answered,replied:g.replied,resolved:g.resolved,first:m,askedAt:v.map(q=>q.asked_at).filter(Boolean).sort(),fromAsks:v.length>0&&v.every(q=>q.ask)}}const ie=new Map(x.map(t=>[t.key,t]));for(const t of x){t.blockedBy=t.kind!=="work"||t.lane==="done"?[]:t.deps.map(y=>ie.get(String(y).includes("/")?y:`${t.wave}/${y}`)).filter(y=>y&&y.lane!=="done"&&!y.reopened).map(lt),Zt(t,X,E),t.lane==="todo"&&t.kind==="work"&&!t.held&&tt(t)&&(t.lane="progress"),t.unpicked=t.kind==="work"&&t.lane==="todo"&&!!Ht(U,t.key),t.unpicked&&(t.state="prototyping"),t.unpicked&&!t.action&&(t.next=zt),t.buildable=Xt(t);const g=t.kind==="work"&&t.lane==="todo";t.unsorted=g&&!!t.fromAsk&&!t.state,t.ready=g&&t.state==="ready"&&!t.blockedBy.length&&!t.action}xn(x,ie,J);const de=new Map((n.items||[]).map(t=>[t.id,t.topic])),se=d(t=>t.kind==="owner-answer"?de.has(t.data?.id)?/^item:(.+)$/.exec(de.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"),ue=d(t=>{const g=se(t);return K.get(g)||g},"cardOf"),ye=d(t=>{const g=t.startsWith("ask:")?t.slice(4):null,y=g&&A(le,g)?.state==="done"?A(le,g).at:null;return[M.get(t),g&&ke.get(g),y,N.get(t)].filter(Boolean).sort().pop()||""},"closedAt"),qe=d(t=>{const g=ue(t);return g===null?!t.reply:!ie.has(g)||!(ye(g)<String(A(p,t.id)))?!1:ee.has(t.id)?ee.get(t.id)==="working":!t.reply&&!K.has(se(t))&&!(String(z.get(g)||"")>String(t.ts))},"unanswered"),oe=o.filter(t=>A(p,t.id)&&qe(t)).map(t=>({id:t.id,kind:t.kind,key:ue(t),data:t.data,since:A(p,t.id)})),Se=e.autoPull===!0;for(const t of x)t.working=oe.some(g=>g.key===t.key),t.kind==="work"&&(t.lane==="progress"&&t.queuedAt||Se&&t.lane==="todo"&&dt(t))&&Object.assign(t,{lane:"todo",queued:!0}),t.kind==="work"&&t.lane==="progress"&&!t.builder&&!t.next&&t.waitsOn!=="checking"&&!t.working&&!t.queued&&(t.next=en),t.sortAt=(t.queued?t.queuedAt||t.created:Bn[t.lane](t))||t.updatedAt||null;x.sort(Bt);const Ee=Sn(e,S,B,x,U,o),be=ne(),ce=yt({on:e.autoReview,incoming:e.incoming,records:e.reviewRecords,events:o,now:u,refs:new Map(x.filter(t=>t.kind==="work").map(t=>[t.key,t.ref]))}),ve=[...qn(x,be,U),...St(ce)],Ie=x.filter(t=>t.ask?.answerAt).sort((t,g)=>g.ask.answerAt.localeCompare(t.ask.answerAt)).map(t=>({key:t.key,id:t.id,text:t.title,at:t.ask.answerAt}));return On({kind:L,main:me,remote:j&&{host:j.host,name:`${j.host}/${j.path}`},waves:S,waveBands:Ee,items:x,moved:Object.fromEntries(K),answers:Ie,owner:U,ownerTopics:be,reviews:ce,needs:ve,working:oe.length,workingOn:oe,checks:An(e,u)},e)}d(Hn,"buildBoard");function On(e,{listen:n,prompts:s}={}){const o=[...En(s),...e.needs];return{...e,needs:o,needsSummary:In(o),checks:{...e.checks,watcher:{listening:"running",asleep:"asleep",waking:"waking"}[n]||"stopped"}}}d(On,"withHub");const lt=d(e=>({key:e.key,ref:e.ref}),"linkOf"),it=d((e,n)=>String(n).includes("/")?String(n):`${e}/${n}`,"linkKey"),$n=d((e,n)=>e.ref.localeCompare(n.ref,void 0,{numeric:!0}),"byRef");function xn(e,n,s){const o=new Map,r=new Map,a=new Map,p=d((c,f,k)=>c.set(f,[...c.get(f)||[],k]),"add");for(const[c,f]of s){f.parent&&p(o,f.parent,c);for(const k of f.related)k!==c&&(p(r,c,k),p(r,k,c))}for(const c of e)for(const f of c.blockedBy||[])p(a,f.key,c.key);const u=d((c=[])=>[...new Set(c)].map(f=>n.get(f)).filter(Boolean).map(lt).sort($n),"cards");for(const c of e){const f=s.get(c.key)?.parent;c.parent=f?{key:f,ref:n.get(f)?.ref||f.slice(f.indexOf("/")+1)}:null,c.children=u(o.get(c.key)),c.related=u(r.get(c.key)),c.waitsOnIt=u(a.get(c.key))}}d(xn,"linkCards");const Bn={todo:d(e=>e.created,"todo"),progress:d(e=>e.progressSince,"progress"),review:d(e=>e.inReviewSince,"review"),stale:d(e=>e.inReviewSince,"stale"),done:d(e=>e.closedAt,"done"),asks:d(e=>e.ask.answerAt,"asks")},H=d((e,n)=>`${e} ${n}${e===1?"":"s"}`,"plural");function An({checked:e,watcher:n,replyAt:s,threads:o={},working:r={},waves:a=[]},p){const u=Number(e?.every)||30,f=[s,n?.since,...Object.values(r),...Object.values(o).flat().filter(i=>(i.who||"claude")==="claude").map(i=>i.ts),...a.flatMap(i=>Object.values(i.agents||{}).flatMap(h=>Object.values(h||{})).map(h=>h?.done||h?.started))].filter(i=>typeof i=="string"&&!Number.isNaN(Date.parse(i))).reduce((i,h)=>i&&Date.parse(i)>Date.parse(h)?i:h,null),k=!!e?.idle;return{at:e?.at||null,every:k?null:u,idle:k,late:!k&&!!e?.at&&p-Date.parse(e.at)>2*u*6e4,claudeAt:f}}d(An,"checksOf");function qn(e,n,s){const o=new Set(e.map(u=>u.questions?.topic).filter(Boolean)),r=d(u=>u.find(c=>s.find(f=>f.id===c)?.state==="open")||u[0],"firstOpen"),a=n.filter(u=>u.state!=="resolved"&&u.open>0&&!o.has(u.id)).map(u=>{const c=`questions.html#${r(u.questions)}`,f=/^(?:lineup|wave):(.+)$/.exec(u.id)?.[1]||null;return{kind:"Question",title:u.title,text:`${u.title} \xB7 ${H(u.open,"question")}`,url:c,topic:u.id,band:f,questions:{open:u.open,url:c},ready:null}}),p=e.filter(u=>!u.held).map(u=>{const c=u.questions&&u.questions.state!=="resolved"&&u.questions.open>0?{open:u.questions.open,url:`questions.html#${u.questions.first}`}:null,f=u.waitsOn==="ready"?u.pr.url:null,k=!c&&!f&&u.action?{label:u.action.label,url:u.action.url}:null;if(!c&&!f&&!k)return null;const i=`${u.ref}: ${u.title}`,h=[i,c&&H(c.open,"question"),f&&"Ready on GitHub",k?.label].filter(Boolean).join(" \xB7 ");return{kind:c?"Question":f?"Tap Ready":"Action needed",title:i,text:h,url:`#item=${encodeURIComponent(u.key)}`,key:u.key,questions:c,ready:f,action:k}}).filter(Boolean);return[...a,...p]}d(qn,"needsOf");function En(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}})}d(En,"promptLines");function In(e){const n=e.filter(a=>a.prompt).length,s=e.reduce((a,p)=>a+(p.questions?.open||0),0),o=e.filter(a=>a.ready).length,r=e.filter(a=>a.review).length;return[n&&H(n,"prompt"),s&&H(s,"question"),o&&`${H(o,"draft")} waiting on Ready`,r&&`${H(r,"review")} held`].filter(Boolean).join(" \xB7 ")}d(In,"needsSummary");function zn(e){const n=d((...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))}}d(zn,"needsAnswer");const dt=d(e=>e.buildable&&e.state==="ready"&&!e.held&&!e.unsorted&&!e.blockedBy.length&&!e.questions?.open&&!e.questions?.answered&&!e.grilling&&!e.prototyping,"queueable"),Mn=10;function Dn(e,{cap:n=Mn,pulled:s=[]}={}){const o=e.filter(w=>!pe(w)),r=d(w=>(w.agents||[]).filter(M=>!M.done),"running"),a=o.filter(w=>w.waitsOn==="checking"||r(w).some(Y)).length,p=o.filter(w=>dt(w)&&!w.working).map(w=>w.key),u=o.filter(w=>s.includes(w.key)&&(p.includes(w.key)||w.buildable&&w.working)).map(w=>w.key),c=d(w=>w.queued&&w.queuedAt&&!w.blockedBy.length,"tapped"),f=o.filter(w=>c(w)||p.includes(w.key)&&!u.includes(w.key)).slice(0,Math.max(0,n-a-u.length)),k=f.filter(c).map(w=>w.key),i=f.filter(w=>!c(w)).map(w=>w.key),O=o.some(w=>w.grilling||w.prototyping||r(w).some(re))?[]:o.filter(w=>(w.buildable||w.unpicked&&!w.action)&&w.state!=="ready"&&!w.held&&!w.questions?.open&&!w.questions?.answered&&!w.working).map(w=>w.key);return{idle:!a,pullable:p,waiting:u,starts:k,pulls:i,plannable:O}}d(Dn,"pullOf");function _n(e){return e.filter(n=>n.lane==="todo"&&(!n.queued||n.buildable)||n.lane==="stale").map(n=>JSON.stringify([n.key,n.title,n.deps||[]])).sort().join(`
|
|
5
|
+
`)}d(_n,"todoOf");function Jn(e){const n=e.filter(o=>o.kind==="work"&&o.lane!=="done"&&o.questions?.open>0),s=n.map(o=>JSON.stringify([o.key,o.questions.open,o.questions.askedAt||[]])).sort().join(`
|
|
6
|
+
`);return{waiting:n.map(o=>o.key),seen:`${s}
|
|
7
|
+
--
|
|
8
|
+
${_n(e)}`}}d(Jn,"triageOf");const Cn=100,Rn=["key","kind","id","ref","title","lane","status","wave","waveTitle","fold","internal","ticket","ticketSource","linear","pr","prs","review","reviewers","risky","sortAt","threadCount","othersAt","hasNotes","questions","next","ask","action","actionButton","builder","working","waitsOn","reachedPhase","grilling","refusal","install","parent","children","related","waitsOnIt","blockedBy","ready","finishable","reopened","manualSteps","branch","worktree"],Zn=d(e=>Object.fromEntries(Rn.filter(n=>n in e).map(n=>[n,e[n]])),"slimDone");function es(e,n=Cn){const s=new Set((e.waveBands||[]).flatMap(r=>r.keys)),o=new Set(e.items.filter(r=>r.lane!=="done").flatMap(r=>[...(r.deps||[]).map(a=>String(a).includes("/")?String(a):`${r.wave}/${a}`),r.parent?.key,...(r.children||[]).map(a=>a.key),...(r.related||[]).map(a=>a.key)]));return e.items.filter(r=>r.lane==="done"&&r.kind==="work").sort((r,a)=>String(a.closedAt||"").localeCompare(String(r.closedAt||""))).slice(n).filter(r=>["done","merged","dropped"].includes(r.status)&&!s.has(r.key)&&!o.has(r.key)&&(!r.questions||r.questions.state==="resolved"))}d(es,"archivedOf");const Nn=d(e=>e.startsWith("ask:")?`ask-${e.slice(4)}`:`item-${e}`,"pushTag");function ts(e){const n=d(a=>e.owner.filter(p=>p.topic===a&&p.state==="open").map(({id:p,question:u})=>({id:p,question:u})),"open"),s=[I.ready,I.merge],o=e.items.filter(a=>a.action).map(a=>{const p=Ze.has(a.action.kind)?n(a.questions.topic):[];return{tag:Nn(a.key),kind:a.action.kind,title:`${a.ref}: ${a.action.label}`,body:p[0]?.question||a.title,url:p.length?a.action.url:`#item=${encodeURIComponent(a.key)}`,now:!s.includes(a.action.kind),questions:p}}),r=e.needs.filter(a=>a.topic).map(a=>{const p=n(a.topic);return{tag:`topic-${a.topic}`,kind:I.questions,title:a.text,body:p[0]?.question||a.title,url:a.url,now:!0,questions:p}});return[...o,...r,...bt(e.reviews)]}d(ts,"ownerActions");function ns(e,n){const s=e.owner.find(a=>a.id===n),o=/^item:([^/:]+\/[^/]+)$/.exec(s?.topic||"")?.[1],r=o&&e.items.find(a=>a.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(a=>!a.done)||e.owner.some(a=>a.topic===s.topic&&a.state==="open")?null:o}d(ns,"startedByAnswer");function ss(e,{pulled:n=[]}={}){return!e.items.filter(r=>!pe(r)).some(r=>r.waitsOn==="checking"||r.grilling||r.prototyping||(r.agents||[]).some(a=>!a.done))&&!e.working&&!Dn(e.items,{pulled:n}).waiting.length}d(ss,"strictlyIdle");const Ln=15;function os(e,{idle:n,now:s}){const o=s.toISOString(),r=e.last||o;return n?e.since?e.pushed||s-Date.parse(e.since)<Ln*6e4?{state:{...e,last:r},push:!1}:{state:{last:o,since:e.since,pushed:!0},push:!0}:{state:{last:r,since:o},push:!1}:{state:{last:r},push:!1}}d(os,"idleStep");function rs(e,n){const s=d(c=>String(c.closedAt||"")>n,"after"),o=e.filter(c=>c.kind==="work"&&c.lane==="done"&&!Ne(c)&&s(c)),r=new Map;for(const c of e)c.lineup&&r.set(c.lineup.slug,[...r.get(c.lineup.slug)||[],c]);const a=[...r.values()].filter(c=>c.every(f=>f.lane==="done")&&c.some(s)).map(c=>c[0].lineup.title),p=o.slice(0,3).map(c=>c.ref).join(", ")+(o.length>3?` and ${o.length-3} more`:"");return[o.length&&`${o.length} ${o.length===1?"card":"cards"} landed: ${p}`,...a.map(c=>`${c} finished`)].filter(Boolean).join(" \xB7 ")||"Nothing closed since the last idle notification."}d(rs,"idleSummary");export{I as ACTION_KIND,Ce as BUILDER_ASK,$e as BUILDER_KIND,qt as BUILDER_STALLED_AFTER,Et as BUILD_REFUSED,F as BUTTON,Rn as DONE_FIELDS,Cn as DONE_KEPT,Ln as IDLE_MINUTES,At as ITEM_EVENTS,Vn as ITEM_STATUSES,Mn as MAX_IMPLEMENTERS,en as NO_NEXT,on as PLANNER_WORD,zt as PROTOTYPE_AGAIN,Z as PR_STATE,es as archivedOf,Hn as buildBoard,Y as builds,os as idleStep,rs as idleSummary,Yn as lineupMoved,vn as lineupPhases,Qt as mergeBlockers,un as modelName,zn as needsAnswer,Ht as noTakePicked,Xn as owesNext,ts as ownerActions,cn as ownerQuestion,Yt as picksNoTake,re as plans,st as providerOf,Dn as pullOf,Nn as pushTag,Re as refOf,ae as roundOf,pn as roundSettledBy,Zn as slimDone,ns as startedByAnswer,ss as strictlyIdle,rt as threadIndex,at as threadOf,_n as todoOf,Jn as triageOf,Mt as waveFold,On as withHub};
|
package/board/finish.mjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
var C=Object.defineProperty;var p=(t,e)=>C(t,"name",{value:e,configurable:!0});import R from"node:fs";import w from"node:path";import{spawnSync as v}from"node:child_process";import*as n from"./store.mjs";import{builderRepo as T,git as l,holdingLandLock as x,land as A,passTests as D,worktreesDir as E}from"./worktree.mjs";import{readPermissions as I}from"./permissions.mjs";import{boardAt as L}from"../hub/boards.mjs";import{boardRepo as N}from"../hub/board-repo.mjs";const m=p(t=>{throw new n.BoardError(t)},"fail"),P=p(t=>typeof t=="string"&&t.trim()?t.trim():m("--text must be non-empty text"),"said");async function G(t,e,{text:r,ownerStep:o,repo:i}){const a=e.split("/")[1],d=await n.recordedWorktree(t,e),{repo:f,base:u,remote:$}=T(t,{repo:i??d?.repo}),s={path:d?.path??w.join(E(),a),branch:d?.branch??a},c=await n.cardName(t,e);return $?B(t,e,{...s,base:u,ref:c,text:P(r)}):x(c,async()=>{await n.checkClose(t,e,r,o),await n.checkCallsVerified(t,e),A({...s,repo:w.resolve(f),base:u,ref:c});const h=await n.setItem(t,e,{status:"done",text:r,ownerStep:o});return{said:o===void 0?"landed and done":`landed; waits on the owner step "${o.trim()}"`,lines:[...h.lines,...await q(t,e,f,u)]}})}p(G,"finishCard");async function q(t,e,r,o){if(!(await I(r)).pushMain)return[];const i=l(r,"push","--quiet","origin",o);if(i.ok)return[`pushed ${o} to origin`];const a=`${o} didn't reach origin: ${i.err}; push it with git -C ${r} push origin ${o}`;return await n.appendThread(t,e,{text:`Landed, but ${a}`}),[`landed, but ${a}`]}p(q,"pushBase");async function B(t,e,{path:r,branch:o,base:i,ref:a,text:d}){await n.checkCallsVerified(t,e),R.existsSync(r)||m(`no worktree at ${r}`),l(r,"status","--porcelain").out&&m(`${r} has uncommitted changes; commit them there first`),Number(l(r,"rev-list","--count",`refs/remotes/origin/${i}..HEAD`).out)===0&&m(`${o} has no commits beyond ${i}; nothing to send for review`),D(r,{outcome:"nothing was pushed"});const f=l(r,"push","--quiet","origin",`HEAD:refs/heads/${o}`);f.ok||m(`could not push ${o} to origin: ${f.err}`);const{github:u}=N(L(t).route);if(!u){const b=await n.setItem(t,e,{pushed:o});return await n.appendThread(t,e,{text:d}),{said:`pushed ${o} for its merge`,lines:b.lines}}const{title:$}=await n.briefFacts(t,e),s=v("gh",["pr","create","--draft","--repo",u,"--base",i,"--head",o,"--title",`${a}: ${$}`,"--body",d],{cwd:r,encoding:"utf8"}),c=`${s.stdout||""}
|
|
2
|
+
${s.stderr||""}`,h=(s.status===0||/already exists/.test(c))&&/\/pull\/(\d+)/.exec(c)?.[1];h||m(`could not open a draft PR for ${o}: ${(s.stderr||s.error?.message||`gh exited ${s.status}`).trim().split(`
|
|
3
|
+
`)[0]}`);const g=await n.setItem(t,e,{pr:Number(h)});return await n.appendThread(t,e,{text:d}),{said:`draft PR #${h} opened`,lines:g.lines}}p(B,"sendForReview");export{G as finishCard};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
var $=Object.defineProperty;var d=(e,r)=>$(e,"name",{value:r,configurable:!0});import u from"node:fs";import g from"node:os";import h from"node:path";import{spawn as w}from"node:child_process";import{fileURLToPath as v}from"node:url";import{claudeBin as R,inherited as k}from"../hub/sessions.mjs";import{boardAt as y}from"../hub/boards.mjs";import{readHubRules as x}from"../hub/rules.mjs";import{builderRepo as j}from"./worktree.mjs";import{appendThread as T,briefFacts as B,readBoardRules as S}from"./store.mjs";import{modelName as A}from"./domain.mjs";const I=v(new URL("./bin/board.mjs",import.meta.url)),m=d(e=>`node ${I} --site ${e}`,"cli"),O=d(e=>`git -c core.commentChar=';' rebase ${e}`,"rebase"),W=d(e=>e.length?["Rules from the hub and this board:",...e.map(r=>`- ${r}`),""]:[],"ruleLines"),f=d(({route:e,key:r,worktree:c,base:o})=>[`- With no unverified calls, done finishes the card itself: it rebases onto ${o}, reruns the full test suite, fast-forwards ${o} and closes the card (on a board with pull requests it pushes the branch and opens a draft PR instead).`,`- On a conflict it undoes its rebase and names the files: run \`${O(o)}\` in ${c}, resolve each conflict keeping both sides' intent, continue with \`git -c core.commentChar=';' rebase --continue\`, rerun the tests, and run done again.`,"- On failing tests, fix them, commit, and run done again.","- While another card is landing it refuses, naming that card: wait a minute and run done again.",`- Finishing never goes to the orchestrator. If done refuses for a reason you can't fix, record it with \`${m(e)} builder call ${r} "<why>"\` and run done again: with the call it hands the card back.`],"finishSteps"),b=d(({route:e,key:r})=>`\`${m(e)} builder done ${r} --text "<what you checked>"\``,"finishCommand");function C({route:e,key:r,ref:c,title:o,notes:i,worktree:s,base:n,rules:t=[]}){const a=m(e);return[`You are the headless builder for card ${c} (${r}, "${o}") on the kickflip board /${e}. The board launched you; nobody watches this session, so never wait on a person. Work only in ${s}. Never restart the hub or a board server, and never push.`,"",`1. Run \`${a} builder start ${r} --kind build\`. If it refuses, stop.`,`2. Report progress with \`${a} builder progress ${r} --phase reading|building|testing|checking|landing "<plain, 80 characters or fewer>"\`, at least every 10 minutes.`,`3. To ask the orchestrator something, run \`${a} builder ask ${r} "<question>"\` and carry on; read its replies with \`${a} builder asks ${r}\`. Never block on a reply.`,`4. Record each judgment call the notes leave open with \`${a} builder call ${r} "<call>"\` instead of guessing silently.`,"",i?`Spec: the card's notes, ${i}, all sections, where later sections override earlier ones.`:"The card has no notes file: build from its title, and record what you had to decide as calls.","",...W(t),"Write tests first, and run the full suite once, in the foreground, at the end. Where the repo keeps ADRs, write one named with a timestamp: docs/adr/YYYY-MM-DD-HHMM-<slug>.md.",`Commit with \`git -c core.commentChar=';' commit -F <file>\`, the subject "${c}: <what the card now does>".`,"",`To finish, run ${b({route:e,key:r})}.`,"- If you recorded a judgment call, it hands the card back for the orchestrator to verify your calls; stop there: the orchestrator's verification finishes the card.",...f({route:e,key:r,worktree:s,base:n}),"","Never push."].join(`
|
|
2
|
+
`)}d(C,"builderBrief");function H({route:e,key:r,ref:c,title:o,worktree:i,base:s,answers:n=[]}){const t=m(e);return[`You are the headless lander for card ${c} (${r}, "${o}") on the kickflip board /${e}. Its build is done and the orchestrator verified its judgment calls; it waits only to land. Nobody watches this session, so never wait on a person. Work only in ${i}, never restart the hub or a board server, and never push.`,"",`1. Run \`${t} builder start ${r} --kind build --label Lander\`. If it refuses, stop.`,`2. Report progress with \`${t} builder progress ${r} --phase landing "<plain, 80 characters or fewer>"\`.`,...n.length?["3. The owner answered the builder's deferred asks. Where an answer changes the build, change it, with its tests, and commit:",...n.map(a=>`- ${a.ask} "${a.question}": ${a.answer}`)]:[],"",`To finish, run ${b({route:e,key:r})}.`,...f({route:e,key:r,worktree:i,base:s}),"","Never push."].join(`
|
|
3
|
+
`)}d(H,"landerBrief");function L({bin:e=R(),cwd:r,prompt:c,model:o,log:i}){u.mkdirSync(h.dirname(i),{recursive:!0});const s=u.openSync(i,"a"),n=Object.fromEntries(Object.entries(process.env).filter(([t])=>!k(t)));return h.isAbsolute(e)&&(n.PATH=`${h.dirname(e)}:${n.PATH||""}`),new Promise((t,a)=>{const l=w(e,["-p",c,"--permission-mode","auto","--model",o],{cwd:r,env:n,detached:!0,stdio:["ignore",s,s]});l.once("spawn",()=>{u.closeSync(s),l.unref(),t({pid:l.pid,log:i})}),l.once("error",p=>{u.closeSync(s),a(p)})})}d(L,"launchHeadless");const N=d(e=>{try{return process.kill(e,0),!0}catch(r){return r.code==="EPERM"}},"pidAlive");async function Q(e,r,{launch:c,handBack:o,alive:i=N}){const s={};for(const[n,t]of Object.entries(e))if(r.has(n)){if(t.pid!==null&&i(t.pid)){s[n]=t;continue}if(t.pid!==null&&t.relaunched){await o(t.model?{key:n,model:t.model}:{key:n});continue}s[n]={model:t.model,...await c({key:n,model:t.model}),relaunched:t.pid!==null}}return s}d(Q,"reviveBuilders");const P=d((e,r)=>h.join(g.homedir(),".claude","kickflip","logs",`${r}-${e}-${new Date().toISOString().replace(/[:.]/g,"-")}.log`),"logFile");async function V(e,{key:r,model:c},{lander:o=!1}={}){const i=await B(e,r),s=y(e)?.route??h.basename(e),n=[...x(),...await S(e)].map(p=>p.text),t={...i,route:s,base:j(e).base,rules:n},a=c||"opus",l=await L({cwd:i.worktree,prompt:o?H(t):C(t),model:a,log:P(r.split("/")[1],o?"lander":"builder")});return await T(e,r,{text:`${o?"Lander":"Builder"} launched headless on ${A({model:a})} in ${i.worktree}.`,log:h.basename(l.log)}),l}d(V,"launchBuilder");export{I as BOARD_CLI,C as builderBrief,H as landerBrief,V as launchBuilder,L as launchHeadless,Q as reviveBuilders};
|
package/board/on-host.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var l=Object.defineProperty;var r=(e,s)=>l(e,"name",{value:s,configurable:!0});import f from"node:os";import{execFile as u}from"node:child_process";import{promisify as m}from"node:util";const g=m(u),p=r(e=>String(e||"").trim().replace(/^::ffff:/,""),"bare"),d=r(()=>Object.values(f.networkInterfaces()).flat().map(e=>e.address),"interfaceAddresses");async function i(){try{const{stdout:e}=await g("tailscale",["status","--json"],{timeout:5e3}),{Self:s,User:n}=JSON.parse(e);return{login:n?.[s.UserID]?.LoginName,addresses:s.TailscaleIPs||[]}}catch{return null}}r(i,"tailnetNode");const c=r(e=>{const s=e.headers["x-forwarded-for"];return p(s?String(s).split(",").at(-1):e.socket.remoteAddress)},"clientOf");async function S(e,s={addresses:d,node:i}){const n=e.headers["x-forwarded-for"],t=c(e);if(s.addresses().includes(t))return!0;const a=e.headers["tailscale-user-login"];if(!n||!a)return!1;const o=await s.node();return!!o&&o.login===a&&o.addresses.includes(t)}r(S,"fromHost");async function h(e,s={addresses:d,node:i}){if(s.addresses().includes(c(e)))return!0;const n=e.headers["tailscale-user-login"];if(!n)return!1;const t=await s.node();return!!t&&t.login===n}r(h,"fromOwner");export{S as fromHost,h as fromOwner};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var S=Object.defineProperty;var a=(s,t)=>S(s,"name",{value:t,configurable:!0});import c from"node:fs/promises";import p from"node:path";import{BoardError as d}from"./store.mjs";const u=[{id:"pushMain",name:"Push main after a land",allows:"Pushes main to origin as each card lands, and lets the session push it.",rule:"Bash(git push origin main)"},{id:"pushTags",name:"Push release tags",allows:"Lets the session push a v* release tag.",rule:"Bash(git push origin v*)"},{id:"draftPr",name:"Open draft PRs",allows:"Lets the session open a PR as a draft.",rule:"Bash(gh pr create --draft *)"}];function f(s){if(typeof s!="string"||!p.isAbsolute(s))throw new d("This board has no repo folder recorded, so it has no settings.local.json to write. Set its repo first.");return p.join(s,".claude","settings.local.json")}a(f,"settingsFile");const m=a(s=>p.join(s,"data","permission-refusals.json"),"refusalsFile");async function y(s,t,e){let n;try{n=await c.readFile(s,"utf8")}catch(i){if(i.code==="ENOENT")return t;throw i}try{const i=JSON.parse(n);if(i&&typeof i=="object"&&!Array.isArray(i))return i}catch{}throw new d(`${e} isn\u2019t a JSON object, so the hub leaves it as it is. Fix it by hand first.`)}a(y,"readJSON");async function g(s,t){await c.mkdir(p.dirname(s),{recursive:!0});const e=`${s}.${process.pid}.${Date.now()}.tmp`;await c.writeFile(e,JSON.stringify(t,null,2)+`
|
|
2
|
+
`),await c.rename(e,s)}a(g,"writeJSON");const j=a(s=>Array.isArray(s.permissions?.allow)?s.permissions.allow:[],"allowOf");async function x(s){const t=j(await y(f(s),{},f(s)));return Object.fromEntries(u.map(e=>[e.id,t.includes(e.rule)]))}a(x,"readPermissions");async function A(s,t,e,n){const i=u.find(l=>l.id===e);if(!i||typeof n!="boolean")throw new d(`Send {id, on} with id one of ${u.map(l=>l.id).join(", ")} and on true or false.`);const r=f(s),o=await y(r,{},r);if(o.permissions!==void 0&&(typeof o.permissions!="object"||Array.isArray(o.permissions)||o.permissions===null))throw new d(`${r}\u2019s permissions isn\u2019t an object, so the hub leaves it as it is. Fix it by hand first.`);const w=j(o);w.includes(i.rule)!==n&&(o.permissions={...o.permissions,allow:n?[...w,i.rule]:w.filter(l=>l!==i.rule)},await g(r,o));const h=await b(t);return h[e]&&(delete h[e],await g(m(t),h)),x(s)}a(A,"setPermission");const b=a(s=>y(m(s),{},m(s)),"readRefusals");function O(s){return typeof s!="string"?null:u.find(t=>{const e=/^Bash\((.*)\)$/.exec(t.rule)[1];return new RegExp(`^${e.split("*").map(n=>n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join(".*")}$`).test(s.trim())})||null}a(O,"permissionFor");async function F(s,t,e,n=new Date){if(e?.tool_name!=="Bash")return null;const i=O(e.tool_input?.command);if(!i||!(await x(s))[i.id])return null;const r=await b(t);return r[i.id]={command:e.tool_input.command,how:e.hook_event_name==="PermissionDenied"?"denied":"asked",at:n.toISOString()},await g(m(t),r),i.id}a(F,"recordRefusal");async function B(s,t,e){const[n,i]=await Promise.all([x(s),b(t)]);return{owner:e,file:f(s),permissions:u.map(r=>({...r,on:n[r.id],refused:n[r.id]&&i[r.id]||null}))}}a(B,"permissionsView");export{u as PERMISSIONS,O as permissionFor,B as permissionsView,x as readPermissions,b as readRefusals,F as recordRefusal,A as setPermission,f as settingsFile};
|
package/board/public/app-arch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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/
|
|
2
|
+
// board server to start one (docs/adr/2026-11-02-1813-the-bulb-runs-an-architecture-review-of-the-boards-repo.md). The bulb glows
|
|
3
3
|
// while a review runs. Needs app-core.js.
|
|
4
4
|
BoardView.part((B) => {
|
|
5
5
|
const { $, state } = B;
|
|
@@ -18,7 +18,7 @@ BoardView.part((B) => {
|
|
|
18
18
|
const lastHtml = last ? `<div class="arch-last"><span>Last review: <b>${esc(last.wave)}</b> · ${esc(day(last.at))}</span>`
|
|
19
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
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
|
|
21
|
+
return `${lastHtml}${waits}<button type="button" class="primary arch-run" data-arch-run${starting ? BoardUI.BUSY : a.waits ? ' disabled' : ''}>Run a review of ${esc(a.repo)}</button>`
|
|
22
22
|
+ '<p class="muted">Claude reads the repo and makes a wave. Its first card asks you one question per finding.</p>';
|
|
23
23
|
}
|
|
24
24
|
|
package/board/public/app-asks.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// New tasks on the board view: the + sheet that sends one as an ask event, and bump, edit and delete for a New card or a
|
|
2
|
-
// pending thread message (docs/adr/
|
|
3
|
-
// docs/adr/
|
|
2
|
+
// pending thread message (docs/adr/2026-10-29-1639-a-pending-ask-can-be-deleted-or-bumped.md,
|
|
3
|
+
// docs/adr/2026-10-29-1639-the-plus-makes-a-new-card.md). Needs app-core.js and app-threads.js.
|
|
4
4
|
BoardView.part((B) => {
|
|
5
|
-
const { $, store, state, toast, quieted, sendDraft
|
|
6
|
-
const { attachButton } = BoardUI;
|
|
5
|
+
const { $, store, state, toast, quieted, sendDraft } = B;
|
|
6
|
+
const { attachButton, waitOn } = BoardUI;
|
|
7
7
|
|
|
8
8
|
// The ask box is a bottom sheet: a <dialog> opened from the + button. Closing keeps the draft.
|
|
9
9
|
const sheet = $('#sheet');
|
|
@@ -62,37 +62,52 @@ BoardView.part((B) => {
|
|
|
62
62
|
B.on($('#sheetClose'), 'click', closeSheet);
|
|
63
63
|
B.on(sheet, 'click', (ev) => { if (ev.target === sheet) closeSheet(); });
|
|
64
64
|
B.on(sheet, 'cancel', (ev) => { ev.preventDefault(); closeSheet(); });
|
|
65
|
+
// Discuss or Quick build, above Send: Discuss, the default, has the session grill or prototype the task at once; Quick
|
|
66
|
+
// build sends the ask with quick: true, and the session marks the card ready. Each send goes back to Discuss
|
|
67
|
+
// (docs/adr/2026-11-04-0131-every-new-task-is-grilled-or-prototyped-at-once.md).
|
|
68
|
+
const CHOICE_HELP = {
|
|
69
|
+
discuss: 'Discuss: Claude grills or prototypes it first, then waits for you.',
|
|
70
|
+
quick: 'Quick build: for changes you’re sure about; Claude marks it ready and builds.',
|
|
71
|
+
};
|
|
72
|
+
let quick = false;
|
|
73
|
+
function choose(isQuick) {
|
|
74
|
+
quick = isQuick;
|
|
75
|
+
$('#askDiscuss').setAttribute('aria-pressed', String(!quick));
|
|
76
|
+
$('#askQuick').setAttribute('aria-pressed', String(quick));
|
|
77
|
+
$('#askChoiceHelp').textContent = CHOICE_HELP[quick ? 'quick' : 'discuss'];
|
|
78
|
+
}
|
|
79
|
+
choose(false);
|
|
80
|
+
B.on(B.root, 'click', (ev) => {
|
|
81
|
+
const b = ev.target.closest('#askDiscuss, #askQuick');
|
|
82
|
+
if (b) choose(b.id === 'askQuick');
|
|
83
|
+
});
|
|
65
84
|
B.on($('#askForm'), 'submit', async (ev) => {
|
|
66
85
|
ev.preventDefault();
|
|
67
86
|
$('#askText').blur();
|
|
68
87
|
const text = $('#askText').value.trim();
|
|
69
88
|
const files = BoardAttach.files('ask', B.site.root);
|
|
70
89
|
if ((!text && !files.length) || BoardAttach.busy('ask', B.site.root)) return;
|
|
71
|
-
$('#askStatus').textContent = '
|
|
90
|
+
$('#askStatus').textContent = '';
|
|
72
91
|
try {
|
|
73
|
-
await sendDraft('draft:ask', $('#askText'), () => $('#askText'), () => B.send('ask', { text, ...(files.length ? { files } : {}) }));
|
|
92
|
+
await waitOn($('#askForm button[type="submit"]'), sendDraft('draft:ask', $('#askText'), () => $('#askText'), () => B.send('ask', { text, ...(files.length ? { files } : {}), ...(quick ? { quick: true } : {}) })));
|
|
74
93
|
BoardAttach.clear('ask', B.site.root);
|
|
94
|
+
choose(false);
|
|
75
95
|
$('#askStatus').textContent = '';
|
|
76
96
|
closeSheet();
|
|
77
97
|
toast('Sent');
|
|
78
98
|
} catch { $('#askStatus').textContent = 'Could not send'; }
|
|
79
99
|
});
|
|
80
100
|
|
|
81
|
-
// A bump moves the New card to the top
|
|
82
|
-
// (docs/adr/
|
|
83
|
-
async function bumpAsk(id) {
|
|
84
|
-
|
|
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); }
|
|
101
|
+
// A bump moves the New card to the top once the server has recorded it, button (its icon, or the prototype button
|
|
102
|
+
// that bumps it) waiting meanwhile (docs/adr/2026-11-03-1850-a-tap-waits-for-the-server-to-record-it.md).
|
|
103
|
+
async function bumpAsk(id, button) {
|
|
104
|
+
try { await waitOn(button, B.send('ask-bump', { id })); toast('Bumped to the top'); } catch (e) { B.failed(e); }
|
|
89
105
|
}
|
|
90
106
|
B.on(B.root, 'click', (ev) => {
|
|
91
107
|
const bump = ev.target.closest('button[data-bump]');
|
|
92
108
|
if (!bump) return;
|
|
93
109
|
ev.preventDefault();
|
|
94
|
-
bump.
|
|
95
|
-
bumpAsk(bump.dataset.bump);
|
|
110
|
+
bumpAsk(bump.dataset.bump, bump);
|
|
96
111
|
});
|
|
97
112
|
B.bumpAsk = bumpAsk;
|
|
98
113
|
// A New card's icons sit on the card; a message in a thread, an ask's included, carries its thread's key.
|
|
@@ -134,23 +149,28 @@ BoardView.part((B) => {
|
|
|
134
149
|
return;
|
|
135
150
|
}
|
|
136
151
|
const cancel = ev.target.closest('.edit-form button[data-edit-cancel]');
|
|
137
|
-
if (cancel) {
|
|
152
|
+
if (cancel) {
|
|
153
|
+
const id = cancel.closest('form').dataset.editForm;
|
|
154
|
+
closeEditor(id);
|
|
155
|
+
BoardAttach.clear('edit:' + id, B.site.root);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
138
158
|
const del = ev.target.closest('button[data-withdraw]');
|
|
139
159
|
if (!del) return;
|
|
140
160
|
ev.preventDefault();
|
|
141
161
|
if (!(await B.confirm('Delete this message? Claude will skip it.'))) return;
|
|
142
162
|
const id = del.dataset.withdraw;
|
|
143
163
|
const { key, msg } = await pendingMessage(del, id);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
B.render();
|
|
147
|
-
if (msg) msg.hidden = true;
|
|
164
|
+
// One request records the withdrawal and hides the message, and the card or message leaves once it has
|
|
165
|
+
// (docs/adr/2026-11-03-1850-a-tap-waits-for-the-server-to-record-it.md).
|
|
148
166
|
try {
|
|
149
|
-
await B.send('ask-withdrawn', { id, ...(key ? { key } : {}) });
|
|
150
|
-
|
|
167
|
+
await waitOn(del, B.send('ask-withdrawn', { id, ...(key ? { key } : {}) }));
|
|
168
|
+
state.edits.delete(id);
|
|
169
|
+
BoardAttach.clear('edit:' + id, B.site.root);
|
|
170
|
+
if (msg) msg.hidden = true;
|
|
151
171
|
toast('Deleted');
|
|
152
172
|
if (key) B.loadThread(key, { force: true });
|
|
153
|
-
} catch
|
|
173
|
+
} catch (e) { B.failed(e); }
|
|
154
174
|
});
|
|
155
175
|
B.on(B.root, 'submit', async (ev) => {
|
|
156
176
|
const form = ev.target.closest('form.edit-form');
|
|
@@ -161,22 +181,21 @@ BoardView.part((B) => {
|
|
|
161
181
|
if (!e) return;
|
|
162
182
|
const text = form.querySelector('textarea').value.trim();
|
|
163
183
|
if (!text) { toast('Write something, or delete the message'); return; }
|
|
164
|
-
|
|
165
|
-
|
|
184
|
+
if (BoardAttach.busy(form.dataset.composer, B.site.root)) return;
|
|
185
|
+
const files = BoardAttach.files(form.dataset.composer, B.site.root);
|
|
186
|
+
// The edit box stays open, its Save waiting, until the server has recorded the edit; then the new text and files
|
|
187
|
+
// show, in the thread and on the New card, and a refusal leaves the box as it was
|
|
188
|
+
// (docs/adr/2026-11-03-1850-a-tap-waits-for-the-server-to-record-it.md).
|
|
189
|
+
e.text = text;
|
|
190
|
+
try {
|
|
191
|
+
await waitOn(form.querySelector('button[type="submit"]'), B.send('ask-edited', { ...e.data, text, ...(files.length ? { files } : {}) }));
|
|
192
|
+
} catch (err) { B.failed(err); return; }
|
|
166
193
|
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)));
|
|
194
|
+
if (before) state.threadCache.set(e.thread, before.map((m) => (m.eventId === id ? { ...m, text, edited: true, files: [...(m.files || []), ...files] } : m)));
|
|
168
195
|
closeEditor(id);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
toast('Edited');
|
|
173
|
-
B.loadThread(e.thread, { force: true });
|
|
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
|
-
}
|
|
196
|
+
BoardAttach.clear(form.dataset.composer, B.site.root);
|
|
197
|
+
toast('Edited');
|
|
198
|
+
B.loadThread(e.thread, { force: true });
|
|
180
199
|
});
|
|
181
200
|
// The asks watch polls every 10 s while an ask waits for its reply, sooner than the board's own refresh.
|
|
182
201
|
let asksSeen = false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The answer banner at the top of the board view: asks Claude answered without making work, whose answer this
|
|
2
|
-
// browser hasn't seen (domain.mjs answers; docs/adr/
|
|
2
|
+
// browser hasn't seen (domain.mjs answers; docs/adr/2026-10-29-1639-every-ask-is-a-card.md). Needs app-core.js.
|
|
3
3
|
BoardView.part((B) => {
|
|
4
4
|
const { $, store, state, seen, markSeen, plural, svg } = B;
|
|
5
5
|
const { esc } = BoardUI;
|
|
@@ -7,7 +7,7 @@ BoardView.part((B) => {
|
|
|
7
7
|
|
|
8
8
|
function unseenAnswers(d) {
|
|
9
9
|
const dismissed = Object.assign(Object.create(null), store.get(DISMISSED, {}));
|
|
10
|
-
return (d.answers || []).filter((a) => seen[a.key] && a.at > seen[a.key] && !(dismissed[a.id] >= a.at)
|
|
10
|
+
return (d.answers || []).filter((a) => seen[a.key] && a.at > seen[a.key] && !(dismissed[a.id] >= a.at));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function renderBanner(d) {
|