@lovek4d/kickflip 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +170 -118
- package/README.md +23 -14
- package/SKILL.md +40 -38
- 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 +213 -74
- package/board/domain.mjs +8 -5
- package/board/finish.mjs +4 -0
- package/board/headless.mjs +4 -0
- package/board/merge3.mjs +1 -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 +39 -103
- package/board/public/app-banner.js +2 -2
- package/board/public/app-cards.js +166 -73
- package/board/public/app-core.js +2 -23
- 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-reader.js +174 -0
- package/board/public/app-snapshot.js +2 -2
- package/board/public/app-threads.js +53 -42
- package/board/public/app.html +4 -1
- package/board/public/app.js +78 -96
- 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 +6 -18
- package/board/public/linear-key.js +6 -4
- package/board/public/log.html +48 -0
- package/board/public/new-task.js +121 -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 +24 -6
- package/board/public/sidebar.css +22 -1
- package/board/public/sidebar.js +64 -31
- package/board/public/style.css +245 -64
- package/board/public/toasts.js +2 -2
- package/board/public/ui-cards.js +205 -75
- package/board/public/ui-enums.js +6 -5
- 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 +61 -29
- package/board/public/ui-sidebar.js +54 -24
- package/board/public/ui-snapshot.js +20 -9
- package/board/public/ui-threads.js +40 -21
- package/board/public/ui.js +116 -18
- package/board/public/view-frame.js +2 -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 +142 -62
- package/board/public/view-settings.js +189 -52
- package/board/review.mjs +1 -1
- package/board/server.mjs +20 -12
- package/board/store.mjs +15 -12
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +21 -15
- 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/front-door.mjs +20 -0
- package/hub/github.mjs +4 -4
- package/hub/home.mjs +1 -1
- package/hub/hub.mjs +11 -14
- 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/owner-checks.mjs +1 -0
- 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/owner-check.mjs +3 -0
- package/shared/repos.mjs +1 -1
- package/shared/tailscale.mjs +1 -0
- 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 ht=Object.defineProperty;var d=(e,t)=>ht(e,"name",{value:t,configurable:!0});import{routeRecord as q}from"../hub/routes.mjs";import yt from"node:fs";import{ticketBadge as Ye,ticketIn as St}from"./tickets.mjs";import{reviewLinks as vt,reviewTime as bt,reviewersOf as Ot}from"./review.mjs";import{remoteFromInput as xt}from"../hub/repos.mjs";import{reviewsOf as $t,reviewNeeds as At,reviewActions as Bt}from"./auto-review.mjs";import{cardPRs as Et,hasPR as Te,leastReady as qt,prKeyOf as _t,prURL as Rt}from"../shared/card-prs.mjs";import ze from"node:vm";import{checkWords as It,parseCheck as Dt}from"../shared/owner-check.mjs";const Le=ze.createContext({});for(const e of["ui-enums.js","ui-order.js"])ze.runInContext(yt.readFileSync(new URL(`./public/${e}`,import.meta.url),"utf8"),Le);const{BUTTON:Y,ACTION_KIND:R,BUILDER_KIND:Re,PR_STATE:oe}=Le.BoardUI,{laneOrder:Ct}=Le.BoardUI,Mt=["item-message","mark-ready","grill-more","prototype-request","reask-request"],rs=["planned","waiting","building","fixing","checking","held","merged","done","dropped"],Nt=30*6e4,Je="build-refused",Se=Object.freeze({ask:"builder-ask",answer:"builder-answer"}),Tt=864e5,de=d(e=>e.lane==="done"||e.lane==="asks","closedLane"),Lt=d(e=>`wave:${e}`,"waveFold"),Pt=7*Tt,Ze=d((e,t)=>({id:e,url:t?.workspace?`https://linear.app/${t.workspace}/issue/${e}`:null}),"linearRef"),jt=d((e,t)=>String(e).replace(/^S(\d+)$/,`${t||"INT"}-$1`),"internalRef"),Pe=d((e,t,s)=>St(s,t.ticket)?t.ticket:e.internal===!0?jt(t.id,s?.prefix):t.id,"refOf"),Ut=d((e,t,s,o)=>Ye(e||(t?{source:"internal",prefix:"INT"}:null),s,o)||(t?{id:String(t),url:null,source:null}:null),"ticketOf");function Wt(e,t,s,o,r,a){const p=(e.status||"").toLowerCase();return p==="merged"||p==="done"||p==="dropped"||!r&&(t?.state==="MERGED"||t?.state==="CLOSED")?"done":s||p==="checking"?"progress":o?"review":t?.state==="OPEN"?t.checks?.state==="fail"&&(!t.isDraft||a)?"progress":"review":t?"progress":"todo"}d(Wt,"laneOf");function Ft(e,t){return e?.state==="MERGED"||t.status==="merged"||t.merged_at?oe.merged:e?.state==="CLOSED"?oe.closed:e?.state==="OPEN"?e.isDraft?oe.draft:oe.open:null}d(Ft,"prStateOf");const je=d(e=>{const t=(e.status||"").toLowerCase();return t==="dropped"||e.pr?.state==="CLOSED"&&t!=="merged"&&t!=="done"},"droppedCard");function Kt(e){if(!e.some(o=>o.kind==="work")||e.some(o=>o.lane!=="done"))return null;const t=e.filter(o=>o.lane==="done"),s=t.filter(je).length;return{merged:t.length-s,dropped:s}}d(Kt,"finishedOf");function Qt(e,t,s){return Object.fromEntries(e.map(o=>{const r=q(t?.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"?q(s,o.name):null,github:o.github||null}]}))}d(Qt,"reviewOf");function Gt(e,t,s,o,r,a,{held:p,needsApproval:c}){return s?.kind===Re.install?"builder":o==="checking"?"checking":s?"builder":p&&e==="review"?"merge":r&&e==="review"?"owner":e==="review"&&t?.state==="OPEN"&&t.isDraft?a?"ci":"ready":e==="review"||e==="stale"?et(t,c)?"merge":"reviewers":null}d(Gt,"waitsOnOf");const et=d((e,t)=>e?.reviewDecision==="APPROVED"||!t&&!["REVIEW_REQUIRED","CHANGES_REQUESTED"].includes(e?.reviewDecision),"reviewed"),Ie=/(^|\/)prototype-[\w-]+\.html/,Vt={reviewers:"Waiting for PR reviewers",merge:"Approved, merges once CI and the merge state allow",ci:"Marked ready for review once CI passes"},De={waiting:"Approved, Merge shows once CI and the merge state allow",ready:"Approved and green: tap Merge",merging:"Merging"},Ue={auto:"Ready, merges once CI and the merge state allow",waiting:"Ready, Merge shows once CI and the merge state allow",ready:"Ready and green: tap Merge"},Ht=d(e=>`Verified: tap Merge to land it on ${e}`,"heldNext"),Xt="Fix the failing CI",tt="Tap Done once it is settled",Yt=["status","agent","ask-link",Se.answer],nt=d(e=>(e.who==="claude"||e.via==="github")&&!Yt.includes(e.kind),"countsUnread"),zt=/^(?:Not merged yet: |Lander launched headless )/;function Jt({slug:e,wave:t,it:s,order:o,prs:r,repo:a,remote:p,base:c,builder:u,reachedPhase:f,agents:k,worktree:l,eta:h,thread:O,events:m,working:I,said:G,askedAt:_,topicItems:V,boardReviewers:H,now:U,bumpedAt:i,holdAsk:$,closedAt:S,mergeAt:B,autoMerge:re,skipDraft:Z,tickets:T}){const ee=r.map(w=>({repo:w.repo,number:w.number,url:Rt(w),...w.gh||{},status:Ft(w.gh,s)})),W=qt(r.map(w=>w.gh)),N=r[W]?.gh||null,F=ee[W]?.status??null,j=!!S&&String(S)>String(s.status_at||"");j&&s.status!=="merged"&&(s={...s,status:"done"});const Oe=[j&&S,s.status_at,s.merged_at,...r.map(w=>w.gh?.mergedAt)].filter(Boolean).map(String).sort().pop()||null,L=String(Oe||""),D=["done","merged"].includes(s.status)||F===oe.merged&&s.status!=="dropped"?[...G.filter(w=>w.kind==="item-message").map(w=>w.ts),..._].map(String).filter(w=>w>L).sort().pop():void 0,pe=s.owner_step&&!j&&s.status!=="dropped"?String(s.owner_step):null,fe=["done","merged"].includes(s.status)||F===oe.merged;D&&(s={...s,status:void 0,next:void 0});const xe=!!D&&G.some(w=>String(w.ts)>L&&(w.state==="waiting"||w.state==="working")),ae=`${e}/${s.id}`,ge=Ot(t,H),$e=Object.fromEntries(Object.entries(vt(t,s,ae,V)).map(([w,Q])=>[w,bt(Q.ts)])),P=Object.values($e).filter(Boolean).sort()[0]||null,te=[...O.map(w=>w.ts),...m.map(w=>w.ts)].sort().pop()||null,Ae=[...O.filter(w=>nt({...w,who:w.who||"claude"})).map(w=>w.ts),...m.map(w=>w.reply?.at)].filter(Boolean).sort(),X=!Te(s)&&s.pushed?.branch?{branch:s.pushed.branch,at:s.pushed.at||null,host:p?.host||null}:null,K=!Te(s)&&s.merge_ready?.branch?{branch:s.merge_ready.branch,at:s.merge_ready.at||null,base:c}:null;let A=Wt(s,N,k.some(w=>w.started&&!w.done&&z(w)),X||K,!!D,Z);D&&A==="todo"&&(A="progress"),A==="review"&&P&&U-Date.parse(P)>Pt&&(A="stale");const le=!!pe&&fe&&A==="done";le&&(A="review");let M=Gt(A,N,u,(s.status||"").toLowerCase(),X,Z,{held:K,needsApproval:ge.length>0});const ke=M==="builder"&&N?.state==="OPEN"&&!N.isDraft,me=A==="progress"&&!M&&N?.state==="OPEN"&&(!N.isDraft||Z),ne=le||!!D&&A==="progress"&&!M&&!me&&!xe;ne&&(M="owner");const we=$&&String($.ts)>String(s.held_at||""),Be=A==="todo"&&(we?!!$.data.hold:s.status==="held"||!!s.held),Ce=Be&&(we?$.data.until:s.pick_up_at)||null,se=A==="todo"||A==="progress",Ee=k.filter(z).map(w=>String(w.started||"")).filter(Boolean).sort()[0]||"",Me=i&&!(Ee&&String(i)<Ee),qe=O.filter(w=>(w.who||"claude")==="claude"&&w.kind!=="status"&&!zt.test(w.text||"")).map(w=>String(w.ts)).sort().pop()||"",ie=[B,K&&s.merge_ready.tapped].filter(Boolean).map(String).sort().pop(),he=M==="merge"&&!!ie&&ie>qe,ye=O.filter(w=>w.kind==="status"&&String(w.ts)>String(ie)).sort((w,Q)=>String(w.ts).localeCompare(String(Q.ts))).at(-1),n=ye?.text.startsWith("Waiting for ")?`${De.merging} \xB7 ${ye.text[0].toLowerCase()}${ye.text.slice(1)}`:De.merging,g=O.filter(w=>w.kind!=="status").sort((w,Q)=>String(w.ts).localeCompare(String(Q.ts))).at(-1),y=A==="todo"&&g?.kind===Je?g.text:null,x=D||k.filter(z).map(w=>w.started).filter(Boolean).map(String).sort().pop()||s.queued_at||["building","fixing","checking"].includes(String(s.status||"").toLowerCase())&&s.status_at||null,b=N?.reviewDecision==="APPROVED",E=X&&M==="owner"?`merge ${X.branch} into ${c}`:K&&M==="merge"?he?n:Ht(c):M==="merge"&&!re?he?n:(b?De:Ue).waiting:M==="merge"&&!b?Ue.auto:Vt[M],C=F===oe.draft?`Review draft PR #${ee[W].number}`:null;return{key:ae,kind:"work",order:o,wave:e,waveTitle:t.title||e,id:s.id,ref:Pe(t,s,T),title:s.title,status:s.status||null,created:s.created||null,reviewSince:s.review_since||P||s.merge_ready?.at||s.pushed?.at||s.created||null,progressSince:x,closedAt:A==="done"?Oe:null,internal:t.internal===!0,repo:t.internal===!0&&!t.repo?null:a,lane:A,waitsOn:M,builder:M==="checking"?null:u,reachedPhase:f,agents:k,worktree:l??null,eta:h,working:I,reopened:!!D,awaitsDone:ne,ownerStep:ne?pe:null,ownerCheck:ne&&pe&&s.owner_check?{words:It(Dt(s.owner_check.check)),broken:s.owner_check.broken||null}:null,queuedAt:A!=="done"&&s.queued_at||null,queued:!1,deps:s.deps||[],held:Be,heldUntil:Ce,risky:!!s.risky,bumpable:se,bumpedAt:se&&Me?i:null,holdable:A!=="done",hasNotes:!!(s.plan||String(s.notes||"").trim()),finishable:A!=="done",merging:he,refusal:y,manualSteps:s.manual_steps||[],fromAsk:s.from_ask||null,next:ke||A==="done"||M==="checking"?null:me?Xt:E||s.next||C||null,branch:s.branch||N?.headRefName||X?.branch||null,pushed:A==="done"||le?null:X,mergeReady:A==="review"?K:null,ticket:T?.source==="linear"&&!s.ticket&&s.linear?.length?null:Ut(T,s.ticket,s.ticket||(t.internal?Pe(t,s,T):s.linear?.[0]),{repo:a,key:ae}),ticketSource:T?.source||null,linear:(s.linear||[]).map(w=>T?.source==="linear"&&Ye(T,w,{key:ae,repo:a})||Ze(w,T)),pr:ee[W]||null,prs:ee,reviewers:ge.map(w=>w.name),review:Qt(ge,N,$e),inReviewSince:P,threadCount:O.length+m.length,lastTs:te,othersAt:Ae,grilling:lt("grill",m,k,_),prototyping:lt("prototype",m,k,_),state:s.state||null,updatedAt:N?.mergedAt||N?.updatedAt||s.merged_at||u?.at||te||null,ask:null,fold:null,action:null,install:A==="done"?null:en(s.install,N),waitFacts:Zt(s,{agents:k,thread:O,events:m,refusedAt:y?g.ts:null})}}d(Jt,"workItem");function Zt(e,{agents:t,thread:s,events:o,refusedAt:r}){const a=new Set(s.filter(f=>f.kind===Se.answer).map(f=>f.ask)),p=s.filter(f=>f.kind===Se.ask&&!a.has(f.ask)).sort((f,k)=>String(f.ts).localeCompare(String(k.ts)))[0],c=d(f=>o.filter(k=>Fe(k,f)).pop()?.ts||null,"requestAt"),u=s.filter(f=>(f.who||"claude")==="claude"&&!["status","agent",Je].includes(f.kind)).map(f=>f.ts);return{statusAt:e.status_at||null,next:e.next||null,ask:p?{at:p.ts,text:p.text}:null,grillAt:c("grill"),prototypeAt:c("prototype"),refusedAt:r,unverified:t.some(f=>f.calls.some(k=>!f.verified||String(k.at)>String(f.verified.at))),writes:[...u,...t.flatMap(f=>[f.verified?.at,f.started])].filter(Boolean).map(String).sort()}}d(Zt,"waitFactsOf");function en(e,t){if(!e?.url)return null;const s=e.sha?String(e.sha).toLowerCase():null,o=t?.headRefOid?String(t.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(en,"installOf");function tn(e,{needsApproval:t=!0}={}){if(e?.state!=="OPEN")return[`PR is ${String(e?.state||"unknown").toLowerCase()}`];const{state:s,pending:o,failing:r}=e.checks||{};return[e.isDraft&&"it is a draft",!et(e,t)&&`review: ${String(e.reviewDecision||"none").toLowerCase().replace(/_/g," ")}`,s==="fail"&&`failing: ${r?.length?r.join(", "):"a check"}`,s==="pending"&&`pending: ${o?J(o,"check"):"checks"}`,e.mergeStateStatus!=="CLEAN"&&`merge state: ${String(e.mergeStateStatus||"unknown").toLowerCase()}`].filter(Boolean)}d(tn,"mergeBlockers");const nn=d((e,t)=>!tn(e,{needsApproval:t}).length,"mergeable");function sn(e,t,s){if(e.held||de(e))return null;const o=`#item=${encodeURIComponent(e.key)}`;if(st(e)){const r=t.get(e.questions.first);return Ie.test(r?.url||"")?{kind:R.pick,url:`questions.html#${r.id}`,label:"Prototypes ready: pick a take"}:{kind:R.questions,url:`questions.html#${e.questions.first}`,label:`Answer ${J(e.questions.open,"question")}`}}return e.waitsOn==="ready"?{kind:R.ready,url:e.pr.url,label:`Review draft PR #${e.pr.number}`}:e.kind==="ask"&&e.waitsOn==="owner"?{kind:R.done,url:o,label:e.next||tt}:e.waitsOn==="merge"&&e.manualSteps.length&&!e.mergeReady?{kind:R.steps,url:o,label:"Run the manual steps, then merge"}:e.waitsOn==="merge"&&(!s||e.mergeReady)&&!e.merging&&e.prs.every(r=>["MERGED","CLOSED"].includes(r.state)||nn(r,e.reviewers.length>0))?{kind:R.merge,url:o,label:"Merge"}:null}d(sn,"actionOf");const st=d(e=>!!(e.questions&&e.questions.state!=="resolved"&&e.questions.open>0),"asking"),We=new Set([R.pick,R.questions]);function ot(e){return de(e)?null:st(e)?Y.openQuestions:e.action?.kind===R.done?Y.markDone:null}d(ot,"actionButtonOf");const on=d(e=>e.kind==="work"&&e.lane==="todo"&&!e.action&&!e.unpicked,"buildableOf"),rn=d((e,t)=>/^item:/.test(e.topic||"")&&Ie.test(e.url||"")&&!(e.options||[]).some(s=>s.id===t),"picksNoTake");function an(e,t){const s=e.filter(a=>a.topic===`item:${t}`),r=[...s.filter(a=>Ie.test(a.url||"")).flatMap(a=>a.answers.map(p=>({q:a,a:p,again:rn(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(an,"noTakePicked");const ln="Prototype again",dn=Object.freeze({[R.pick]:Y.openQuestions,[R.questions]:Y.openQuestions,[R.ready]:Y.github,[R.steps]:Y.github,[R.merge]:Y.merge,[R.done]:Y.markDone});function un(e,t,s){if(e.action=sn(e,t,s),e.action?.kind===R.questions&&e.questions.fromAsks){Object.assign(e,{action:null,waitsOn:"owner",next:e.action.label,actionButton:ot(e),resolvedBy:null});return}e.awaitsDone&&=!e.action,e.awaitsDone||Object.assign(e,{ownerStep:null,ownerCheck:null}),e.actionButton=ot(e),e.resolvedBy=dn[e.action?.kind]||null,e.action?.kind===R.merge&&!e.mergeReady&&(e.next=(e.pr?.reviewDecision==="APPROVED"?De:Ue).ready),e.awaitsDone&&Object.assign(e,{lane:"review",next:e.ownerStep||tt,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"&&We.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(un,"placeOwnerWait");const rt=d(e=>e.queuedAt||e.agents.some(z)||["building","fixing"].includes(String(e.status||"").toLowerCase()),"started"),at="none set yet";function as(e,t){const s=String(e.status||"").toLowerCase();if(e.next||Te(e)||e.pushed||e.held||e.queued_at||["held","checking","done","merged","dropped"].includes(s))return!1;const o=t.filter(r=>r?.started);return!o.some(r=>!r.done)&&rt({status:s,agents:o})}d(as,"owesNext");function cn(e,t=new Date){const s=new Date(e),o=s.toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"});return s.toDateString()===t.toDateString()?o:`${s.getMonth()+1}/${s.getDate()} ${o}`}d(cn,"askedLabel");function pn(e,t,s,o,r,a,p){const c=t.find(S=>S.kind==="ask"&&S.eventId===e.id),u=c.files,f=(c.text||"").trim().split(`
|
|
2
|
+
`)[0]||u.map(S=>S.name).join(", ")||"Ask",k=c.state==="replied"||c.state==="answered",l=r?.state==="progress"&&!(String(a||"")>String(r.at||"")),h=!l&&(!!r||!!a),O=k&&!h&&!l&&p.open>0,m=k&&!h&&!l&&!O&&p.answered>0,I=h||k&&!l&&!O&&!m,G=k&&!o&&!r?.moved&&[...t.filter(S=>S.who==="claude"&&S.kind!=="ask-link"&&S.kind!=="status").map(S=>S.ts),...t.map(S=>S.reply?.at)].filter(Boolean).sort().pop()||null,_=I||l||O||m,V=t.some(S=>S.who==="love"&&S.kind!=="ask"&&(S.state==="waiting"||S.state==="working")),H=_?"answered":c.state,U=_?null:s||null,i=[...t.filter(nt).map(S=>S.ts),...t.map(S=>S.reply?.at)].filter(Boolean).sort(),$=[...t.map(S=>S.ts),...i,...l?[r.at]:[]].filter(Boolean).sort().pop();return{key:`ask:${e.id}`,kind:"ask",order:0,wave:null,waveTitle:null,id:e.id,ref:_?cn(e.ts):"New",isNew:!_,title:f,status:null,internal:!1,repo:null,lane:l?"progress":I?"asks":"todo",waitsOn:l&&!V?"owner":null,builder:null,agents:[],eta:null,working:t.some(S=>S.state==="working"),deps:[],held:!1,risky:!1,bumpable:!_,bumpedAt:U,manualSteps:[],fromAsk:null,from:null,next:l&&r.next||null,hasNotes:!1,branch:null,finishable:_&&!I,linear:[],pr:null,prs:[],reviewers:[],review:{},inReviewSince:null,reviewSince:null,progressSince:l&&r.at||null,closedAt:null,threadCount:t.length,lastTs:$,othersAt:_?i:[],grilling:!1,prototyping:!1,created:e.ts,updatedAt:_?$:e.ts,ask:{eventId:e.id,state:H,askedAt:e.ts,bumpedAt:U,edited:!!c.edited,canEdit:!_,text:c.text,files:u,answerAt:G,from:c.from||null},fold:null,action:null}}d(pn,"askItem");const fn=new Set(["grill","prototype"]),ue=d(e=>fn.has(e.kind),"plans"),z=d(e=>!ue(e),"builds"),gn={grill:"preheating",prototype:"prototyping"},kn={grill:"grill-more",prototype:"prototype-request"},Fe=d((e,t)=>e.kind===kn[t]&&!e.data?.question,"isRequest");function lt(e,t,s,o){if(s.some(a=>a.kind===e&&a.started&&!a.done))return!0;const r=t.filter(a=>Fe(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(lt,"planningOf");const ls=30*6e4;function is(e,t){const s=[];for(const o of e.items){if(de(o)||(o.agents||[]).some(a=>a.kind==="prototype"&&a.started&&!a.done))continue;const r=o.prototyping&&t.filter(a=>a.data?.key===o.key&&Fe(a,"prototype")).pop();r&&s.push({key:o.key,request:r.id,since:r.ts});for(const a of e.owner.filter(p=>p.topic===`item:${o.key}`&&p.state==="requested"))s.push({key:o.key,request:a.request.id,question:a.id,since:a.request.ts})}return s}d(is,"prototypeWaits");function Ke(e={}){return Object.entries(e).filter(([,t])=>t&&typeof t=="object").map(([t,s])=>({name:t,kind:it(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((t,s)=>String(t.started||"").localeCompare(String(s.started||""))||t.name.localeCompare(s.name))}d(Ke,"agentsOf");function mn(e={}){return Object.values(e).filter(t=>t?.worktree?.path).sort((t,s)=>String(t.started||"").localeCompare(String(s.started||""))).at(-1)?.worktree.path??null}d(mn,"worktreeOf");function wn(e,t=[]){const s=e.filter(z).map(o=>String(o.started||"")).filter(Boolean).sort().pop();return s===void 0?null:t.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(wn,"reachedPhaseOf");function hn(e,t=[],s){const o=e.filter(l=>l.started&&!l.done),r=o.every(ue),a=r?o:o.filter(z),p=a.map(l=>String(l.started||"")).sort()[0];if(p===void 0)return null;const c=t.filter(l=>l.kind==="status"&&String(l.ts)>=p&&(r||!l.planner)).sort((l,h)=>String(l.ts).localeCompare(String(h.ts))).pop(),u=a.some(l=>l.kind===Re.install)?Re.install:it(a.at(-1).kind),f=c?.ts||p;return{progress:c?c.phase||c.planner?c.text.replace(": "," \xB7 "):c.text||null:r?gn[u]:null,phase:c?.phase||null,at:f,kind:u,planner:r,silentSince:f,stalled:s-Date.parse(f)>=Nt}}d(hn,"builderOf");const Qe={opus:"Claude Opus",sonnet:"Claude Sonnet",codex:"Codex"},yn=d(({model:e,kind:t})=>Qe[e]||Qe[t]||(t==="claude"?Qe.opus:null),"modelName"),it=d(e=>e==="opus"||e==="sonnet"?"claude":e,"providerOf");function Ge(e){const t=yn(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(t&&ue(e))return`${t} ${s}`;if(t)return`${t} ${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(Ge,"agentName");const dt=d((e,t)=>!!e.resolved&&(!e.resolved_at||String(e.resolved_at)>=String(t)),"resolvedSince");function Sn(e,t,s=[],o=[]){const r=t.filter(l=>l.data?.id===e.id),a=r[r.length-1]||null,p=!!(e.resolved&&e.resolved_at&&a&&String(a.ts)>e.resolved_at),c=e.resolved&&!p,u=s.filter(l=>l.data?.question===e.id&&String(l.ts)>String(a?.ts||"")).at(-1),f=c?o.filter(l=>l.data?.question===e.id&&String(l.ts)>String(e.resolved_at||"")).at(-1):null,k=c?"resolved":u?"requested":a?a.reply?"replied":"answered":"open";return{...e,state:k,request:u?{id:u.id,ts:u.ts,note:u.data.note||"",choice:u.data.choice||null}:null,reask:f?{id:f.id,ts:f.ts}:null,answers:r.map(l=>({ts:l.ts,choice:l.data.choice||null,note:l.data.note||"",files:l.data.files||[],reply:l.reply,acted:dt(e,l.ts)}))}}d(Sn,"ownerQuestion");const ce=d(e=>e.round??(Ie.test(e.url||"")?1:0),"roundOf");function vn(e,t){const s=d((o,r)=>o.confirm_of||(o.options||[]).some(a=>a.id===r.choice),"settles");return e.filter(o=>ce(o)===t&&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(vn,"roundSettledBy");function bn(e){const t=e.find(r=>r.request&&!e.some(a=>ce(a)>ce(r)));if(t)return{n:ce(t)+1,state:"making"};const s=Math.max(0,...e.map(ce));if(!s)return null;const o=vn(e,s);return o?{n:s,state:"closed",take:o.choice}:e.some(r=>ce(r)===s&&r.state!=="resolved")?{n:s,state:"back"}:null}d(bn,"roundState");const ve=d((e,t,s)=>{const o=e.get(t);o?o.push(s):e.set(t,[s])},"add"),be=d((e,t)=>{const s=new Map;for(const o of e)ve(s,t(o),o);return s},"groupBy");function On(e,t,s=new Set){const o=new Set(e.map(p=>p.id)),r=[...e];t.some(p=>!o.has(p.topic))&&r.push({id:"",title:"Other"});const a=be(t,p=>o.has(p.topic)?p.topic:"");return r.map(p=>{const c=a.get(p.id)||[],u=d(l=>c.filter(h=>h.state===l).length,"count"),f=c.every(l=>l.state==="resolved")||s.has(p.id)&&!u("open"),k=c.length&&f?"resolved":"open";return{...p,state:k,questions:c.map(l=>l.id),open:u("open"),answered:u("answered"),replied:u("replied"),resolved:u("resolved"),round:bn(c)}}).filter(p=>p.questions.length)}d(On,"ownerTopics");const ut={"mark-ready":"Mark ready","grill-more":"grill requested","prototype-request":"prototype requested","reask-request":"re-ask requested"},xn="answers submitted",$n="quick build: no grill, as you asked",An=d((e,t)=>{if(e.kind!=="prototype-request"||!t.has(e.data.question))return e.data.text||(e.data.files?.length?"":q(ut,e.kind))||"";const s=t.get(e.data.question),o=(s.options||[]).find(r=>r.id===e.data.choice);return[s.question,o&&`${o.id}. ${o.label}`,e.data.note].filter(Boolean).join(`
|
|
3
|
+
\u2192 `)},"actionText"),Bn=d((e,t)=>e.kind!=="grill-more"&&e.kind!=="prototype-request"?e.kind==="reask-request"?{request:!0}:{}:(e.data.question?t.has(e.data.question):String(e.data.text||"").trim()||e.data.files?.length)?{status:ut[e.kind]}:{request:!0},"requestOf"),Ve=d(e=>[...e.filter(t=>t.who==="claude"&&!["status","agent",Se.answer].includes(t.kind)).map(t=>t.ts),...e.filter(t=>t.kind==="ask").map(t=>t.reply?.at)].filter(Boolean).map(String).sort().pop()||"","claudeAtOf");function ct(e,t={}){const s=new Map,o=new Map;for(const k of t.items||[])ve(s,k.topic,k),o.set(k.id,k.topic);const r=new Map,a=new Map,p=new Map,c=new Map,u=new Map,f=new Map;e.forEach((k,l)=>{f.set(k,l),k.data?.key&&Mt.includes(k.kind)&&ve(r,k.data.key,k),k.kind==="owner-answer"&&o.has(k.data?.id)&&ve(a,o.get(k.data.id),k),k.kind==="ask"&&p.set(k.id,k)});for(const k of e.filter(l=>l.kind==="ask-edited").sort((l,h)=>String(l.ts).localeCompare(String(h.ts)))){c.set(k.data?.id,k.data?.text);for(const l of k.data?.files||[])ve(u,k.data?.id,l)}return{byKey:r,answers:a,asks:p,edits:c,editFiles:u,questions:s,order:f}}d(ct,"threadIndex");const He=d((e,t)=>{const s=t.filter(o=>o?.length);return s.length>1?s.flat().sort((o,r)=>e.order.get(o)-e.order.get(r)):[...s[0]||[]]},"inLogOrder"),En=/; its log is \S*\/\.claude\/kickflip\/logs\/([^/\s]+\.log)\.$/,qn=d(e=>{const t=!e.log&&typeof e.text=="string"?En.exec(e.text):null;return t?{...e,text:`${e.text.slice(0,t.index)}.`,log:t[1]}:e},"withLog");function pt(e,{lines:t=[],events:s=[],owner:o={},working:r={},fromAsk:a=null,links:p=[],agents:c={},moved:u=[],index:f=ct(s,o)}){const k=e.startsWith("ask:")?[e.slice(4)]:[a,...u].filter(Boolean),l=[...new Set([e,...k.map(i=>`ask:${i}`)])],h=new Map((f.questions.get(`item:${e}`)||[]).map(i=>[i.id,i])),O=f.answers.get(`item:${e}`)||[],m=new Set([...h.values()].map(i=>i.round_of).filter(Boolean)),I=Ke(c).filter(z).map(i=>String(i.started||"")).filter(Boolean),G=new Set(O.filter(i=>dt(h.get(i.data.id),i.ts)||m.has(i.data.id)||I.some($=>$>String(i.ts))).map(i=>i.id)),_=[...Ke(c).flatMap(i=>[...i.started?[{ts:i.started,who:"claude",kind:"agent",text:`${Ge(i)} started`,finished:!1,eta:i.done?null:i.eta,...ue(i)?{planner:!0}:{}}]:[],...i.done?[{ts:i.done,who:"claude",kind:"agent",text:`${Ge(i)} finished`,finished:!0,eta:null,...ue(i)?{planner:!0}:{}}]:[],...i.calls.length?[{ts:i.done||i.calls.at(-1).at,who:"claude",kind:"calls",text:Ge(i),calls:i.calls.map($=>$.text),verified:i.verified}]:[]]),...t.map(qn).map(i=>({ts:i.ts,who:i.who||"claude",...i.log?{log:i.log}:{},...i.kind==="status"?{kind:"status",...i.planner?{planner:!0}:{}}:{},...Object.values(Se).includes(i.kind)?{kind:i.kind,ask:i.ask}:{},text:i.text||"",html:i.html||null,link:i.link||null,via:i.via||null})),...He(f,l.map(i=>f.byKey.get(i))).map(i=>({ts:i.ts,who:"love",via:"site",kind:i.kind,text:An(i,h),files:i.data.files||[],eventId:i.id,reply:i.reply,...Bn(i,h)})),...O.map(i=>{const $=[i.data.label||i.data.choice,i.data.note].filter(Boolean).join(" \xB7 ");return{ts:i.ts,who:"love",via:"questions",kind:i.kind,text:`${h.get(i.data.id).question}
|
|
4
|
+
\u2192 ${$}`,files:i.data.files||[],eventId:i.id,reply:i.reply,status:xn}}),...He(f,[...new Set(k)].map(i=>f.asks.has(i)?[f.asks.get(i)]:[])).flatMap(i=>[{ts:i.ts,who:"love",via:"ask",kind:i.kind,text:i.data?.text||"",files:i.data?.files||[],eventId:i.id,reply:i.reply,...u.includes(i.id)?{moved:!0}:{},...typeof i.data?.from?.title=="string"?{from:i.data.from.title}:{}},...i.data?.quick===!0?[{ts:i.ts,who:"love",kind:"quick-build",request:!0,text:$n}]:[],...p.map($=>({ts:i.ts,who:"claude",kind:"ask-link",text:`${$.became?"Became":"Added to"} ${$.ref}`,link:`#item=${encodeURIComponent($.key)}`,html:null,via:null}))])].sort((i,$)=>String(i.ts).localeCompare(String($.ts))),V=Ve(_),{edits:H,editFiles:U}=f;for(const i of _){if(!i.eventId)continue;(i.kind==="ask"||i.kind==="item-message")&&typeof H.get(i.eventId)=="string"&&Object.assign(i,{text:H.get(i.eventId),edited:!0,files:[...i.files,...U.get(i.eventId)||[]]});const $=i.kind==="ask"&&(p.length>0||!!i.moved);i.state=i.reply?"replied":$||G.has(i.eventId)||V>String(i.ts)?"answered":q(r,i.eventId)?"working":"waiting",i.canDelete=i.kind==="item-message"&&(i.state==="working"||i.state==="waiting"),i.canEdit=i.canDelete}return _}d(pt,"threadOf");function _n(e,t,s,o,r,a){const p=d((l,h)=>{const O=a.filter(I=>I.kind==="wave-triage"&&I.data?.[l]===h).pop(),m=r.filter(I=>I.topic===`${l}:${h}`&&(I.state==="open"||I.state==="answered")).length;return{running:!!(O&&!O.reply),questions:m}},"triageOf"),c=new Map(o.map(l=>[l.key,l])),u=d(l=>l.filter(h=>h.lane==="done"&&!je(h)).length,"merged"),f=s.map(l=>({l,cards:l.keys.map(h=>c.get(h)).filter(Boolean)})).map(l=>({...l,open:!l.l.finished_at&&l.cards.some(h=>h.lane!=="done")})).sort((l,h)=>Number(l.open)-Number(h.open));for(const{l,cards:h}of f)for(const O of h)O.lineup={slug:l.slug,title:l.title};const k=f.filter(l=>l.open).map(({l,cards:h})=>({kind:"lineup",slug:l.slug,title:l.title,goal:l.goal,keys:h.map(O=>O.key),phases:l.phases.map(O=>O.filter(m=>c.has(m))).filter(O=>O.length),next:Rn(h),merged:u(h),total:h.length,triage:p("lineup",l.slug)}));return e.fromWaves?[...k,...t.filter(l=>!l.finished).map(l=>({w:l,cards:o.filter(h=>h.wave===l.slug&&h.kind!=="ask")})).filter(({cards:l})=>l.length).map(({w:l,cards:h})=>({kind:"wave",slug:l.slug,title:l.title,goal:null,keys:h.map(O=>O.key),phases:null,next:null,merged:u(h),total:h.length,triage:p("wave",l.slug)}))]:k}d(_n,"waveBandsOf");function Rn(e){const t=e.filter(a=>a.lane==="todo"),s=t.find(a=>a.ready&&a.buildable)||t[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(Rn,"nextOf");function ds(e,t,s,o){const r=e.waveBands.find(l=>l.kind==="lineup"&&l.slug===t);if(!r)return null;const a=new Map(e.items.map(l=>[l.key,l.lane])),p=r.keys.filter(l=>a.get(l)==="todo"),c=p.indexOf(s),u=c+o;if(c<0||u<0||u>=p.length)return null;[p[c],p[u]]=[p[u],p[c]];let f=0;const k=r.keys.map(l=>a.get(l)==="todo"?p[f++]:l);return f=0,r.phases.map(l=>k.slice(f,f+=l.length))}d(ds,"lineupMoved");const In=d(e=>e.phases||e.items.map(t=>[t]),"lineupPhases");function us(e){const{owner:t={},topicItems:s={},events:o=[],threads:r={},prs:a={},working:p={}}=e,c=e.now??Date.now(),u=ct(o,t),f=new Set(o.filter(n=>n.kind==="ask-withdrawn"&&!n.data?.key).map(n=>n.data?.id)),k=new Map(o.filter(n=>n.kind==="ask"&&!f.has(n.id)).map(n=>[n.id,n])),l=d((n,g)=>new Map(o.filter(y=>y.kind===n).sort((y,x)=>String(y.ts).localeCompare(String(x.ts))).map(y=>[y.data?.[g],y.ts])),"latest"),h=l("ask-bump","id"),O=l("card-unbump","key"),m=new Map([...l("card-bump","key")].filter(([n,g])=>!(String(O.get(n)||"")>String(g)))),I=l("card-done","key"),G=l("card-merge","key"),_=e.autoMerge===!0,V=e.tickets,H=new Map(o.filter(n=>n.kind==="card-hold").sort((n,g)=>String(n.ts).localeCompare(String(g.ts))).map(n=>[n.data?.key,n])),U=new Map,i=e.askLinks||{},$=new Map;for(const[n,g]of Object.entries(i))k.has(n)&&$.set(g,[...$.get(g)||[],n]);const S=[],B=[],re=new Map,Z=new Map,T=new Map,ee=new Map,W=e.github?null:xt(e.remote);for(const{slug:n,wave:g,agents:y={}}of e.waves){const x=e.github||g.repo||null;S.push({slug:n,title:g.title||n,status:g.status||null,internal:!!g.internal,finished:null,reviewers:(g.reviewers||[]).map(b=>b.name)}),g.items.forEach((b,E)=>{const C=`${n}/${b.id}`,w=($.get(C)||[]).filter(v=>v!==b.from_ask),Q=[b.from_ask,...w].filter(v=>k.has(v)),mt=new Set(Q.map(v=>`ask:${v}`)),Xe=[...q(r,C)||[],...Q.flatMap(v=>q(r,`ask:${v}`)||[])];b.from_ask&&U.set(b.from_ask,[...U.get(b.from_ask)||[],{key:C,ref:Pe(g,b,V),became:!0}]);const _e=pt(C,{lines:Xe,events:o,owner:t,working:p,fromAsk:b.from_ask||null,moved:w,index:u});for(const v of _e)v.eventId&&re.set(v.eventId,v.state);Z.set(C,Ve(_e)),B.push(Jt({slug:n,wave:g,it:b,order:E,repo:x,remote:W,base:e.branch||"main",prs:Et(b,x).map(v=>({...v,gh:q(a,_t(v))||null})),...(v=>({agents:v,builder:hn(v,q(r,C),c),reachedPhase:wn(v,q(r,C)),eta:v.map(wt=>wt.eta).filter(Boolean).sort().pop()||null}))(Ke(q(y,b.id))),worktree:mn(q(y,b.id)),thread:Xe,events:[...He(u,[C,...mt].map(v=>u.byKey.get(v))),...Q.map(v=>k.get(v))],working:_e.some(v=>v.state==="working"),said:_e.filter(v=>v.who==="love"&&(v.kind==="item-message"||v.kind==="owner-answer")),askedAt:(u.questions.get(`item:${C}`)||[]).filter(v=>v.asked_at).map(v=>v.asked_at),topicItems:s,boardReviewers:e.reviewers||[],now:c,bumpedAt:m.get(C),holdAsk:H.get(C),closedAt:I.get(C),mergeAt:G.get(C),autoMerge:_,skipDraft:e.skipDraft===!0,tickets:V}));const Ne=B[B.length-1];ee.set(C,{parent:b.parent?gt(n,b.parent):null,related:(b.related||[]).map(v=>gt(n,v))}),Ne.lane==="done"&&T.set(C,Ne.closedAt||String(Ne.updatedAt||""))})}const N=new Map(B.map(n=>[n.key,n])),F=new Map,j=e.try||{},Oe=e.autoBuild===!0,L=["web","app"].includes(j.kind)?j.kind:e.waves.some(({wave:n})=>n.items.some(g=>g.install?.url))?"app":"web";for(const n of B){if(n.kind!=="work"||n.lane==="done")continue;const g=q(j.cards,n.key)||{},y=g.head||n.pr?.headRefOid,x=L==="web"?g.sha:n.install?.sha,b=n.builder?.kind===Re.install?"running":g.state||(L==="app"&&n.install?"ready":null);n.try={kind:L,state:b,stale:!!(x&&y&&!String(y).toLowerCase().startsWith(String(x).toLowerCase())),url:L==="web"?null:n.install?.url||null,port:L==="web"&&b==="ready"&&g.https||null,sha:x||null,changes:g.ahead>0,script:g.script===!0,autoBuild:Oe}}const D=j.main,pe=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:j.pull?{running:j.pull.running===!0,error:j.pull.error||null}:null}:null,fe=e.askProgress||{},xe=l("ask-done","id"),ae=be(o.filter(n=>n.kind==="owner-answer"),n=>n.data?.id),ge=be(o.filter(n=>n.kind==="prototype-request"),n=>n.data?.question),$e=be(o.filter(n=>n.kind==="reask-request"),n=>n.data?.question),P=(t.items||[]).map(n=>Sn(n,ae.get(n.id)||[],ge.get(n.id)||[],$e.get(n.id)||[])),te=new Map(P.map(n=>[n.id,n])),Ae=be(P,n=>n.topic),X=d(n=>{const g=Ae.get(n)||[];return{open:g.filter(y=>y.state==="open").length,answered:g.filter(y=>y.state==="answered").length}},"pendingOn");for(const n of k.values()){const g=`ask:${n.id}`,y=N.get(q(i,n.id)),x=[...U.get(n.id)||[]];if(y&&!x.some(E=>E.key===y.key)&&x.push({key:y.key,ref:y.ref,became:!1}),x.length){F.set(g,x[0].key);continue}if(q(i,n.id)){F.set(g,q(i,n.id));continue}const b=pt(g,{lines:q(r,g)||[],events:o,working:p,links:x,index:u});for(const E of b)E.eventId&&re.set(E.eventId,E.state);Z.set(g,Ve(b)),B.push(pn(n,b,h.get(n.id),x.length>0||!!q(i,n.id),q(fe,n.id),xe.get(n.id),X(`item:${g}`)))}for(const n of S){const g=B.filter(y=>y.wave===n.slug);if(n.finished=Kt(g),n.finished)for(const y of g)y.lane==="done"&&(y.fold=Lt(n.slug))}for(const n of B)n.lineup=null;const K=new Set((t.topics||[]).map(n=>n.id)),A=e.waves.flatMap(({slug:n,wave:g})=>(g.lineups||[]).map(y=>{const x=In(y).map(b=>b.map(E=>`${n}/${E}`));return{...y,phases:x,keys:x.flat()}})),le=d(()=>On([...t.topics||[],...B.filter(n=>!K.has(`item:${n.key}`)).map(n=>({id:`item:${n.key}`,card:n.key,title:`${n.ref}: ${n.title}`,...n.kind==="ask"?{askedAt:n.ask.askedAt}:{},summary:"Questions about this work item."})),...A.filter(n=>!K.has(`lineup:${n.slug}`)).map(n=>({id:`lineup:${n.slug}`,title:n.title,summary:"Wave triage."})),...(e.fromWaves?S:[]).filter(n=>!K.has(`wave:${n.slug}`)).map(n=>({id:`wave:${n.slug}`,title:n.title,summary:"Wave triage."}))],P,new Set(B.filter(de).map(n=>`item:${n.key}`))),"topicsOf"),M=new Map(le().map(n=>[n.id,n]));for(const n of B){const g=M.get(`item:${n.key}`);if(!g){n.questions=null;continue}const y=g.questions.filter(E=>te.get(E)?.state!=="resolved"),x=g.questions.find(E=>te.get(E)?.state==="open")||y[0]||g.questions[0],b=g.questions.map(E=>te.get(E)).filter(E=>E?.state==="open");n.questions={topic:g.id,state:g.state,open:g.open,answered:g.answered,replied:g.replied,resolved:g.resolved,first:x,askedAt:b.map(E=>E.asked_at).filter(Boolean).sort(),fromAsks:b.length>0&&b.every(E=>E.ask)}}const ke=new Map(B.map(n=>[n.key,n]));for(const n of B){n.blockedBy=n.kind!=="work"||n.lane==="done"?[]:n.deps.map(y=>ke.get(String(y).includes("/")?y:`${n.wave}/${y}`)).filter(y=>y&&y.lane!=="done"&&!y.reopened).map(ft),un(n,te,_),n.lane==="todo"&&n.kind==="work"&&!n.held&&rt(n)&&(n.lane="progress"),n.unpicked=n.kind==="work"&&n.lane==="todo"&&!!an(P,n.key),n.unpicked&&(n.state="prototyping"),n.unpicked&&!n.action&&(n.next=ln),n.buildable=on(n);const g=n.kind==="work"&&n.lane==="todo";n.unsorted=g&&!!n.fromAsk&&!n.state,n.ready=g&&n.state==="ready"&&!n.blockedBy.length&&!n.action}Mn(B,ke,ee);const me=new Map((t.items||[]).map(n=>[n.id,n.topic])),ne=d(n=>n.kind==="owner-answer"?me.has(n.data?.id)?/^item:(.+)$/.exec(me.get(n.data.id))?.[1]||null:"":n.kind==="ask"?`ask:${n.id}`:n.data?.key||(n.kind.startsWith("ask-")&&n.data?.id?`ask:${n.data.id}`:null),"ownCardOf"),we=d(n=>{const g=ne(n);return F.get(g)||g},"cardOf"),Be=d(n=>{const g=n.startsWith("ask:")?n.slice(4):null,y=g&&q(fe,g)?.state==="done"?q(fe,g).at:null;return[I.get(n),g&&xe.get(g),y,T.get(n)].filter(Boolean).sort().pop()||""},"closedAt"),Ce=d(n=>{const g=we(n);return g===null?!n.reply:!ke.has(g)||!(Be(g)<String(q(p,n.id)))?!1:re.has(n.id)?re.get(n.id)==="working":!n.reply&&!F.has(ne(n))&&!(String(Z.get(g)||"")>String(n.ts))},"unanswered"),se=o.filter(n=>q(p,n.id)&&Ce(n)).map(n=>({id:n.id,kind:n.kind,key:we(n),data:n.data,since:q(p,n.id)})),Ee=e.autoPull===!0;for(const n of B)n.working=se.some(g=>g.key===n.key),n.kind==="work"&&(n.lane==="progress"&&n.queuedAt||Ee&&n.lane==="todo"&&kt(n))&&Object.assign(n,{lane:"todo",queued:!0}),n.kind==="work"&&n.lane==="progress"&&!n.builder&&!n.next&&n.waitsOn!=="checking"&&!n.working&&!n.queued&&(n.next=at),n.sortAt=(n.queued?n.queuedAt||n.created:Nn[n.lane](n))||(["review","stale"].includes(n.lane)?null:n.updatedAt)||null;for(const n of B){if(n.kind!=="work")continue;const g=(Ae.get(`item:${n.key}`)||[]).filter(x=>x.state==="answered").map(x=>x.answers.at(-1).ts).sort().pop(),y=se.filter(x=>x.key===n.key).map(x=>String(x.since)).sort()[0];n.orchestrator=["progress","todo"].includes(n.lane)?Vn(n,n.waitFacts,{answeredAt:g,workingSince:y}):null,delete n.waitFacts}B.sort(Ct);const Me=_n(e,S,A,B,P,o),qe=le(),ie=$t({on:e.autoReview,incoming:e.incoming,records:e.reviewRecords,events:o,now:c,refs:new Map(B.filter(n=>n.kind==="work").map(n=>[n.key,n.ref]))}),he=[...Ln(B,qe,P),...At(ie)],ye=B.filter(n=>n.ask?.answerAt).sort((n,g)=>g.ask.answerAt.localeCompare(n.ask.answerAt)).map(n=>({key:n.key,id:n.id,text:n.title,at:n.ask.answerAt}));return Dn({kind:L,main:pe,remote:W&&{host:W.host,name:`${W.host}/${W.path}`},waves:S,waveBands:Me,items:B,moved:Object.fromEntries(F),answers:ye,owner:P,ownerTopics:qe,reviews:ie,needs:he,working:se.length,workingOn:se,checks:Tn(e,c)},e)}d(us,"buildBoard");function Dn(e,{listen:t,prompts:s}={}){const o=[...Pn(s),...e.needs];return{...e,needs:o,needsSummary:jn(o),checks:{...e.checks,watcher:{listening:"running",asleep:"asleep",waking:"waking"}[t]||"stopped"}}}d(Dn,"withHub");const ft=d(e=>({key:e.key,ref:e.ref}),"linkOf"),gt=d((e,t)=>String(t).includes("/")?String(t):`${e}/${t}`,"linkKey"),Cn=d((e,t)=>e.ref.localeCompare(t.ref,void 0,{numeric:!0}),"byRef");function Mn(e,t,s){const o=new Map,r=new Map,a=new Map,p=d((u,f,k)=>u.set(f,[...u.get(f)||[],k]),"add");for(const[u,f]of s){f.parent&&p(o,f.parent,u);for(const k of f.related)k!==u&&(p(r,u,k),p(r,k,u))}for(const u of e)for(const f of u.blockedBy||[])p(a,f.key,u.key);const c=d((u=[])=>[...new Set(u)].map(f=>t.get(f)).filter(Boolean).map(ft).sort(Cn),"cards");for(const u of e){const f=s.get(u.key)?.parent;u.parent=f?{key:f,ref:t.get(f)?.ref||f.slice(f.indexOf("/")+1)}:null,u.children=c(o.get(u.key)),u.related=c(r.get(u.key)),u.waitsOnIt=c(a.get(u.key))}}d(Mn,"linkCards");const Nn={todo:d(e=>e.created,"todo"),progress:d(e=>e.progressSince,"progress"),review:d(e=>e.reviewSince,"review"),stale:d(e=>e.reviewSince,"stale"),done:d(e=>e.closedAt,"done"),asks:d(e=>e.ask.answerAt,"asks")},J=d((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural");function Tn({checked:e,watcher:t,replyAt:s,threads:o={},working:r={},waves:a=[]},p){const c=Number(e?.every)||30,f=[s,t?.since,...Object.values(r),...Object.values(o).flat().filter(l=>(l.who||"claude")==="claude").map(l=>l.ts),...a.flatMap(l=>Object.values(l.agents||{}).flatMap(h=>Object.values(h||{})).map(h=>h?.done||h?.started))].filter(l=>typeof l=="string"&&!Number.isNaN(Date.parse(l))).reduce((l,h)=>l&&Date.parse(l)>Date.parse(h)?l:h,null),k=!!e?.idle;return{at:e?.at||null,every:k?null:c,idle:k,late:!k&&!!e?.at&&p-Date.parse(e.at)>2*c*6e4,claudeAt:f}}d(Tn,"checksOf");function Ln(e,t,s){const o=new Set(e.map(c=>c.questions?.topic).filter(Boolean)),r=d(c=>c.find(u=>s.find(f=>f.id===u)?.state==="open")||c[0],"firstOpen"),a=t.filter(c=>c.state!=="resolved"&&c.open>0&&!o.has(c.id)).map(c=>{const u=`questions.html#${r(c.questions)}`,f=/^(?:lineup|wave):(.+)$/.exec(c.id)?.[1]||null;return{kind:"Question",title:c.title,text:`${c.title} \xB7 ${J(c.open,"question")}`,url:u,topic:c.id,band:f,questions:{open:c.open,url:u},ready:null}}),p=e.filter(c=>!c.held).map(c=>{const u=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,k=!u&&!f&&c.action?{label:c.action.label,url:c.action.url}:null;if(!u&&!f&&!k)return null;const l=`${c.ref}: ${c.title}`,h=[l,u&&J(u.open,"question"),f&&"Ready on GitHub",k?.label].filter(Boolean).join(" \xB7 ");return{kind:u?"Question":f?"Tap Ready":"Action needed",title:l,text:h,url:`#item=${encodeURIComponent(c.key)}`,key:c.key,questions:u,ready:f,action:k}}).filter(Boolean);return[...a,...p]}d(Ln,"needsOf");function Pn(e=[]){return e.map(t=>{const s=t.questions?.length?`Question: ${t.questions.map(o=>o.question).join(" \xB7 ")}`:`${t.tool}: ${t.detail||t.action}`;return{kind:"Prompt",title:s,text:s,url:"",prompt:t}})}d(Pn,"promptLines");function jn(e){const t=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[t&&J(t,"prompt"),s&&J(s,"question"),o&&`${J(o,"draft")} waiting on Ready`,r&&`${J(r,"review")} held`].filter(Boolean).join(" \xB7 ")}d(jn,"needsSummary");function cs(e){const t=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:t("review","stale"),progress:t("progress"),todo:t("todo")},building:e.items.some(s=>(s.agents||[]).some(o=>!o.done))}}d(cs,"needsAnswer");const kt=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"),Un=10;function Wn(e,{cap:t=Un,pulled:s=[]}={}){const o=e.filter(m=>!de(m)),r=d(m=>(m.agents||[]).filter(I=>!I.done),"running"),a=o.filter(m=>m.waitsOn==="checking"||r(m).some(z)).length,p=o.filter(m=>kt(m)&&!m.working).map(m=>m.key),c=o.filter(m=>s.includes(m.key)&&(p.includes(m.key)||m.buildable&&m.working)).map(m=>m.key),u=d(m=>m.queued&&m.queuedAt&&!m.blockedBy.length,"tapped"),f=o.filter(m=>u(m)||p.includes(m.key)&&!c.includes(m.key)).slice(0,Math.max(0,t-a-c.length)),k=f.filter(u).map(m=>m.key),l=f.filter(m=>!u(m)).map(m=>m.key),O=o.some(m=>m.grilling||m.prototyping||r(m).some(ue))?[]:o.filter(m=>(m.buildable||m.unpicked&&!m.action)&&m.state!=="ready"&&!m.held&&!m.questions?.open&&!m.questions?.answered&&!m.working).map(m=>m.key);return{idle:!a,pullable:p,waiting:c,starts:k,pulls:l,plannable:O}}d(Wn,"pullOf");function Fn(e){return e.filter(t=>t.lane==="todo"&&(!t.queued||t.buildable)||t.lane==="stale").map(t=>JSON.stringify([t.key,t.title,t.deps||[]])).sort().join(`
|
|
5
|
+
`)}d(Fn,"todoOf");function ps(e){const t=e.filter(o=>o.kind==="work"&&o.lane!=="done"&&o.questions?.open>0),s=t.map(o=>JSON.stringify([o.key,o.questions.open,o.questions.askedAt||[]])).sort().join(`
|
|
6
|
+
`);return{waiting:t.map(o=>o.key),seen:`${s}
|
|
7
|
+
--
|
|
8
|
+
${Fn(e)}`}}d(ps,"triageOf");const Kn=100,Qn=["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"],fs=d(e=>Object.fromEntries(Qn.filter(t=>t in e).map(t=>[t,e[t]])),"slimDone");function gs(e,t=Kn){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(t).filter(r=>["done","merged","dropped"].includes(r.status)&&!s.has(r.key)&&!o.has(r.key)&&(!r.questions||r.questions.state==="resolved"))}d(gs,"archivedOf");const Gn=d(e=>e.startsWith("ask:")?`ask-${e.slice(4)}`:`item-${e}`,"pushTag");function ks(e){const t=d(a=>e.owner.filter(p=>p.topic===a&&p.state==="open").map(({id:p,question:c})=>({id:p,question:c})),"open"),s=[R.ready,R.merge],o=e.items.filter(a=>a.action).map(a=>{const p=We.has(a.action.kind)?t(a.questions.topic):[];return{tag:Gn(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=t(a.topic);return{tag:`topic-${a.topic}`,kind:R.questions,title:a.text,body:p[0]?.question||a.title,url:a.url,now:!0,questions:p}});return[...o,...r,...Bt(e.reviews)]}d(ks,"ownerActions");function ms(e,t){const s=e.owner.find(a=>a.id===t),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(ms,"startedByAnswer");function ws(e,{pulled:t=[]}={}){return!e.items.filter(r=>!de(r)).some(r=>r.waitsOn==="checking"||r.grilling||r.prototyping||(r.agents||[]).some(a=>!a.done))&&!e.working&&!Wn(e.items,{pulled:t}).waiting.length}d(ws,"strictlyIdle");function Vn(e,t,{answeredAt:s,workingSince:o}){const r=d(f=>e.agents.some(k=>k.kind===f&&k.started&&!k.done),"running"),a=[e.progressSince,...e.agents.map(f=>f.done)].filter(Boolean).map(String).sort().pop()||null,p=t.ask?{kind:"ask",since:t.ask.at,next:`Answer the builder's ask: ${t.ask.text}`}:s?{kind:"answer",since:s,next:"Act on the owner's answer"}:We.has(e.action?.kind)?null:e.waitsOn==="checking"?t.unverified?{kind:"calls",since:t.statusAt,next:t.next||"Verify the builder's judgment calls"}:{kind:"checking",since:t.statusAt,next:t.next||"Land the checked build"}:e.grilling&&t.grillAt&&!r("grill")?{kind:"grill",since:t.grillAt,next:"Start the grill asked for"}:e.prototyping&&t.prototypeAt&&!r("prototype")?{kind:"prototype",since:t.prototypeAt,next:"Draw the prototype asked for"}:e.refusal&&t.refusedAt?{kind:"refused",since:t.refusedAt,next:e.refusal}:e.next===at&&a?{kind:"unowned",since:a,next:"Start a builder on it, or set its Next"}:null;if(!p?.since)return null;const c=String(p.since),u=[t.writes.find(f=>f>c),o].filter(Boolean).sort()[0]||null;return{kind:p.kind,since:c,picked:u,next:p.next}}d(Vn,"orchestratorWaitOf");function hs(e){return e.items.filter(t=>t.orchestrator).map(t=>({key:t.key,ref:t.ref,title:t.title,...t.orchestrator}))}d(hs,"orchestratorWaits");const ys=30*6e4,Hn=15;function Ss(e,{idle:t,now:s}){const o=s.toISOString(),r=e.last||o;return t?e.since?e.pushed||s-Date.parse(e.since)<Hn*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(Ss,"idleStep");function vs(e,t){const s=d(u=>String(u.closedAt||"")>t,"after"),o=e.filter(u=>u.kind==="work"&&u.lane==="done"&&!je(u)&&s(u)),r=new Map;for(const u of e)u.lineup&&r.set(u.lineup.slug,[...r.get(u.lineup.slug)||[],u]);const a=[...r.values()].filter(u=>u.every(f=>f.lane==="done")&&u.some(s)).map(u=>u[0].lineup.title),p=o.slice(0,3).map(u=>u.ref).join(", ")+(o.length>3?` and ${o.length-3} more`:"");return[o.length&&`${o.length} ${o.length===1?"card":"cards"} landed: ${p}`,...a.map(u=>`${u} finished`)].filter(Boolean).join(" \xB7 ")||"Nothing closed since the last idle notification."}d(vs,"idleSummary");export{R as ACTION_KIND,Se as BUILDER_ASK,Re as BUILDER_KIND,Nt as BUILDER_STALLED_AFTER,Je as BUILD_REFUSED,Y as BUTTON,Qn as DONE_FIELDS,Kn as DONE_KEPT,Hn as IDLE_MINUTES,Mt as ITEM_EVENTS,rs as ITEM_STATUSES,Un as MAX_IMPLEMENTERS,at as NO_NEXT,ys as ORCHESTRATOR_PUSH_AFTER,gn as PLANNER_WORD,ln as PROTOTYPE_AGAIN,ls as PROTOTYPE_WAIT,oe as PR_STATE,gs as archivedOf,us as buildBoard,z as builds,Ss as idleStep,vs as idleSummary,ds as lineupMoved,In as lineupPhases,tn as mergeBlockers,yn as modelName,cs as needsAnswer,an as noTakePicked,hs as orchestratorWaits,as as owesNext,ks as ownerActions,Sn as ownerQuestion,rn as picksNoTake,ue as plans,is as prototypeWaits,it as providerOf,Wn as pullOf,Gn as pushTag,Pe as refOf,ce as roundOf,vn as roundSettledBy,fs as slimDone,ms as startedByAnswer,ws as strictlyIdle,ct as threadIndex,pt as threadOf,Fn as todoOf,ps as triageOf,Lt as waveFold,Dn as withHub};
|
package/board/finish.mjs
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var N=Object.defineProperty;var u=(t,a)=>N(t,"name",{value:a,configurable:!0});import W from"node:fs";import x from"node:path";import{spawnSync as j}from"node:child_process";import*as n from"./store.mjs";import{builderRepo as M,git as b,joiningLand as v,landInTurn as A,passTests as E,syncBase as B,worktreesDir as D}from"./worktree.mjs";import{readPermissions as F}from"./permissions.mjs";import{launchBuilder as H}from"./headless.mjs";import{boardAt as C}from"../hub/boards.mjs";import{boardRepo as R}from"../hub/board-repo.mjs";const w=u(t=>{throw new n.BoardError(t)},"fail"),L=u(t=>typeof t=="string"&&t.trim()?t.trim():w("--text must be non-empty text"),"said");async function I(t,a,{text:r,ownerStep:o,check:e,repo:i,onWait:d}){const m=a.split("/")[1],p=await n.recordedWorktree(t,a),{repo:c,base:s,remote:l}=M(t,{repo:i??p?.repo}),h={path:p?.path??x.join(D(),m),branch:p?.branch??m,repo:x.resolve(c),base:s},f=await n.cardName(t,a),g=await n.readBoardSettings(t),$=l?R(C(t).route).github:null;if(l&&(!$||g.prs))return V(t,a,{...h,ref:f,text:L(r)});const T=await v(f,()=>P(t,a,{tree:h,ref:f,base:s,remote:l,repo:c,settings:g,text:r,ownerStep:o,check:e,onWait:d}));return T.joined?await n.isFinished(t,a)?{said:"landed and done",lines:[]}:I(t,a,{text:r,ownerStep:o,check:e,repo:i,onWait:d}):T}u(I,"finishCard");async function P(t,a,{tree:r,ref:o,base:e,remote:i,repo:d,settings:m,text:p,ownerStep:c,check:s,onWait:l}){await n.checkClose(t,a,p,c,s),await n.checkCallsVerified(t,a),i&&(B(r.repo,e),b(r.repo,"branch","--unset-upstream",r.branch));const h=u($=>n.appendThread(t,a,{text:`Waiting for the test slot: ${$} is running the suite`,kind:"status"}),"onWaitTests"),f=m.auto_merge||!!(await n.heldMerge(t,a))?.tapped;if(await A({...r,ref:o},{hold:!f,onWait:l,onWaitTests:h,sync:u(()=>i&&B(r.repo,e),"sync")}),!f){const $=await n.setItem(t,a,{mergeReady:r.branch,ownerStep:c,check:s});return await n.appendThread(t,a,{text:p}),{said:"verified; waits for your Merge",lines:$.lines}}const g=await n.setItem(t,a,{status:"done",text:p,ownerStep:c,check:s});return{said:c===void 0?"landed and done":`landed; waits on the owner step "${c.trim()}"`,lines:[...g.lines,...await q(t,a,d,e)]}}u(P,"landCard");async function Y(t,a,{launch:r=H}={}){(await n.heldMerge(t,a))?.tapped||w(`${a} isn't waiting on a Merge tap`);try{const{base:e}=M(t,{repo:(await n.recordedWorktree(t,a))?.repo}),i=u(d=>n.appendThread(t,a,{text:`Waiting for ${d} to land`,kind:"status"}),"onWait");return await I(t,a,{text:`Landed on ${e} on your Merge.`,onWait:i})}catch(e){if(!(e instanceof n.BoardError))throw e;const i=e.message.split(`
|
|
2
|
+
`)[0];if(/^rebasing .+ conflicts in |^tests failed in /.test(i)){await n.appendThread(t,a,{text:`Not merged yet: ${i}. A lander fixes it and lands it.`});try{await r(t,{key:a,model:await n.builderModel(t,a)},{lander:!0})}catch(d){await n.appendThread(t,a,{text:`Couldn't launch the lander headless (${d.message}); land it with board.mjs land ${a}.`})}}else await n.appendThread(t,a,{text:`Not merged: ${i}. Tap Merge again.`});throw e}}u(Y,"landHeld");async function q(t,a,r,o){if(!(await F(r)).pushMain)return[];const e=b(r,"push","--quiet","origin",o);if(e.ok)return[`pushed ${o} to origin`];const i=`${o} didn't reach origin: ${e.err}; push it with git -C ${r} push origin ${o}`;return await n.appendThread(t,a,{text:`Landed, but ${i}`}),[`landed, but ${i}`]}u(q,"pushBase");async function V(t,a,{path:r,branch:o,base:e,ref:i,text:d}){await n.checkCallsVerified(t,a),W.existsSync(r)||w(`no worktree at ${r}`),b(r,"status","--porcelain").out&&w(`${r} has uncommitted changes; commit them there first`),Number(b(r,"rev-list","--count",`refs/remotes/origin/${e}..HEAD`).out)===0&&w(`${o} has no commits beyond ${e}; nothing to send for review`),await E(r,{outcome:"nothing was pushed"});const m=b(r,"push","--quiet","origin",`HEAD:refs/heads/${o}`);m.ok||w(`could not push ${o} to origin: ${m.err}`);const{github:p}=R(C(t).route);if(!p){const g=await n.setItem(t,a,{pushed:o});return await n.appendThread(t,a,{text:d}),{said:`pushed ${o} for its merge`,lines:g.lines}}const{title:c}=await n.briefFacts(t,a),s=j("gh",["pr","create","--draft","--repo",p,"--base",e,"--head",o,"--title",`${i}: ${c}`,"--body",d],{cwd:r,encoding:"utf8"}),l=`${s.stdout||""}
|
|
3
|
+
${s.stderr||""}`,h=(s.status===0||/already exists/.test(l))&&/\/pull\/(\d+)/.exec(l)?.[1];h||w(`could not open a draft PR for ${o}: ${(s.stderr||s.error?.message||`gh exited ${s.status}`).trim().split(`
|
|
4
|
+
`)[0]}`);const f=await n.setItem(t,a,{pr:Number(h)});return await n.appendThread(t,a,{text:d}),{said:`draft PR #${h} opened`,lines:f.lines}}u(V,"sendForReview");export{I as finishCard,Y as landHeld};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
var q=Object.defineProperty;var d=(t,e)=>q(t,"name",{value:e,configurable:!0});import g from"node:fs";import A from"node:os";import $ from"node:path";import{spawn as O}from"node:child_process";import{fileURLToPath as L}from"node:url";import{claudeBin as N,inherited as P}from"../hub/sessions.mjs";import{boardAt as S}from"../hub/boards.mjs";import{readHubRules as F}from"../hub/rules.mjs";import{builderRepo as W}from"./worktree.mjs";import{appendThread as Y,briefFacts as D,readBoardRules as H}from"./store.mjs";import{modelName as M,plans as E}from"./domain.mjs";const _=L(new URL("./bin/board.mjs",import.meta.url)),b=d(t=>`node ${_} --site ${t}`,"cli"),K=d(t=>`git -c core.commentChar=';' rebase ${t}`,"rebase"),T=d(t=>t.length?["Rules from the hub and this board:",...t.map(e=>`- ${e}`),""]:[],"ruleLines"),j=d(({route:t,key:e,worktree:n,base:a})=>[`- With no unverified calls, done finishes the card itself: it rebases onto ${a}, reruns the full test suite, fast-forwards ${a} and closes the card (on a board with pull requests it pushes the branch and opens a draft PR instead; with Auto-merge off it stops before the fast-forward, and the card waits for the owner's Merge).`,`- On a conflict it undoes its rebase and names the files: run \`${K(a)}\` in ${n}, 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, done waits its turn and then lands; leave it running.",`- Finishing never goes to the orchestrator. If done refuses for a reason you can't fix, record it with \`${b(t)} builder call ${e} "<why>"\` and run done again: with the call it hands the card back.`],"finishSteps"),B=d(({route:t,key:e})=>`\`${b(t)} builder done ${e} --text "<what you checked>"\``,"finishCommand");function U({route:t,key:e,ref:n,title:a,notes:c,worktree:i,base:s,rules:r=[]}){const o=b(t);return[`You are the headless builder for card ${n} (${e}, "${a}") on the kickflip board /${t}. The board launched you; 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 \`${o} builder start ${e} --kind build\`. If it refuses, stop.`,`2. Report progress with \`${o} builder progress ${e} --phase reading|building|testing|checking|landing "<plain, 80 characters or fewer>"\`, at least every 10 minutes.`,`3. To ask the orchestrator something, run \`${o} builder ask ${e} "<question>"\` and carry on; read its replies with \`${o} builder asks ${e}\`. Never block on a reply.`,`4. Record each judgment call the notes leave open with \`${o} builder call ${e} "<call>"\` instead of guessing silently.`,"",c?`Spec: the card's notes, ${c}, 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.","",...T(r),"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 "${n}: <what the card now does>".`,"",`To finish, run ${B({route:t,key:e})}.`,"- 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.",...j({route:t,key:e,worktree:i,base:s}),"","Never push."].join(`
|
|
2
|
+
`)}d(U,"builderBrief");function X({route:t,key:e,ref:n,title:a,worktree:c,base:i,answers:s=[]}){const r=b(t);return[`You are the headless lander for card ${n} (${e}, "${a}") on the kickflip board /${t}. 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 ${c}, never restart the hub or a board server, and never push.`,"",`1. Run \`${r} builder start ${e} --kind build --label Lander\`. If it refuses, stop.`,`2. Report progress with \`${r} builder progress ${e} --phase landing "<plain, 80 characters or fewer>"\`.`,...s.length?["3. The owner answered the builder's deferred asks. Where an answer changes the build, change it, with its tests, and commit:",...s.map(o=>`- ${o.ask} "${o.question}": ${o.answer}`)]:[],"",`To finish, run ${B({route:t,key:e})}.`,...j({route:t,key:e,worktree:c,base:i}),"","Never push."].join(`
|
|
3
|
+
`)}d(X,"landerBrief");function G({route:t,key:e,ref:n,title:a,notes:c,kind:i,repo:s,rules:r=[],text:o,files:u=[],question:l,note:p,choice:w}){const h=b(t),f=i==="grill";return[`You are the headless ${f?"grill":"prototype"} planner for card ${n} (${e}, "${a}") on the kickflip board /${t}. The board launched you; nobody watches this session, so never wait on a person: the owner answers on the board, and a later planner or builder picks up from their answers. Read and run in ${s}; write no output code there, never restart the hub or a board server, and never push.`,"",`1. Run \`${h} builder start ${e} --kind ${i}\`. If it refuses, stop.`,`2. Gather the context yourself: ${c?`the card's notes, ${c}, all sections, where later sections override earlier ones; `:"the card has no notes file yet; "}its thread, in the board site's data/threads folder, the repo's docs/adr and CONTEXT.md, and the cards its notes and thread link.`,`3. Report progress with \`${h} builder progress ${e} "<plain, 80 characters or fewer>"\` at least every 10 minutes.`,f?`4. Follow the grilling skill: find every decision the card leaves open, and post each as a question for the owner with \`${h} question add --file <json>\` on topic item:${e}, one per decision, your recommendation first.`:`4. Follow the prototype skill: record the prototype with \`${h} prototype add <topic> --card ${e} --take <K>:<label> \u2026\`, write one take per option, built from the board's real components fed fake data, and post one question whose url opens it with \`${h} question add --file <json>\` on topic item:${e}.`,...o?[` The owner asked for this ${f?"grill":"prototype"}, writing: "${o}"`]:[],...u.length?[` The owner attached: ${u.map(m=>m.path).join(", ")}.`]:[],...l?[` The owner asked for prototypes of question ${l}${p?`, noting "${p}"`:""}${w?`, leaning to option ${w}`:""}: draw that question's next round, its question naming round_of ${l}.`]:[],`5. Record what you learned in the card's notes with \`${h} item set ${e} --notes-file <file.md>\`, naming each answered question's id.`,"",...T(r),`To finish, run \`${h} builder done ${e} --text "<what you asked>"\`.`,"","Never push."].join(`
|
|
4
|
+
`)}d(G,"plannerBrief");const z=d(()=>process.env.KICKFLIP_CODEX||"codex","codexBin"),J=d(({kind:t,prompt:e,model:n,cwd:a})=>t==="codex"?["exec","--sandbox","danger-full-access","--cd",a,e]:["-p",e,"--permission-mode","auto","--model",n,"--output-format","stream-json","--verbose"],"agentArgs");function Q({kind:t="claude",bin:e=t==="codex"?z():N(),cwd:n,prompt:a,model:c,log:i}){g.mkdirSync($.dirname(i),{recursive:!0});const s=g.openSync(i,"a"),r=Object.fromEntries(Object.entries(process.env).filter(([o])=>!P(o)));return $.isAbsolute(e)&&(r.PATH=`${$.dirname(e)}:${r.PATH||""}`),new Promise((o,u)=>{const l=O(e,J({kind:t,prompt:a,model:c,cwd:n}),{cwd:n,env:r,detached:!0,stdio:["ignore",s,s]});l.once("spawn",()=>{g.closeSync(s),l.unref(),o({pid:l.pid,log:i})}),l.once("error",p=>{g.closeSync(s),u(p)})})}d(Q,"launchHeadless");const V=d(t=>{try{return process.kill(t,0),!0}catch(e){return e.code==="EPERM"}},"pidAlive");async function ue(t,e,{launch:n,handBack:a,alive:c=V}){const i={};for(const[s,r]of Object.entries(t)){if(!e.has(s))continue;if(r.pid!==null&&c(r.pid)){i[s]=r;continue}const o={key:s,...r.kind?{kind:r.kind}:{},...r.model?{model:r.model}:{}};if(r.pid!==null&&r.relaunched){await a(o);continue}const{key:u,...l}=o;i[s]={...l,...await n(o),relaunched:r.pid!==null}}return i}d(ue,"reviveBuilders");const Z=d((t,e)=>$.join(A.homedir(),".claude","kickflip","logs",`${e}-${t}-${new Date().toISOString().replace(/[:.]/g,"-")}.log`),"logFile");async function pe(t,{key:e,kind:n="claude",model:a,text:c,files:i,question:s,note:r,choice:o},{lander:u=!1}={}){const l=await D(t,e),p=S(t)?.route??$.basename(t),w=[...F(),...await H(t)].map(I=>I.text),{base:h,repo:f}=W(t),m=E({kind:n}),v={...l,route:p,base:h,repo:f,rules:w,kind:n,text:c,files:i,question:s,note:r,choice:o},x=n==="codex"?"codex":a||"opus",y=m?"Planner":u?"Lander":"Builder",R=m?f:l.worktree,C=m?G(v):u?X(v):U(v),k=await Q({kind:m?"claude":n,cwd:R,prompt:C,model:x,log:Z(e.split("/")[1],y.toLowerCase())});return await Y(t,e,{text:`${y} launched on ${M({kind:n,model:x})} in ${R}.`,log:$.basename(k.log)}),k}d(pe,"launchBuilder");export{_ as BOARD_CLI,U as builderBrief,X as landerBrief,pe as launchBuilder,Q as launchHeadless,V as pidAlive,G as plannerBrief,ue as reviveBuilders};
|
package/board/merge3.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var A=Object.defineProperty;var d=(o,t)=>A(o,"name",{value:t,configurable:!0});import{isDeepStrictEqual as y}from"node:util";const p=d(o=>o!==null&&typeof o=="object"&&!Array.isArray(o),"isObject"),g=d(o=>Array.isArray(o)&&o.every(t=>p(t)&&typeof t.id=="string")&&new Set(o.map(t=>t.id)).size===o.length,"byId");function j(o,t,f){if(y(t,o))return f;if(y(f,o))return t;if(p(o)&&p(t)&&p(f)){const c={};for(const n of new Set([...Object.keys(f),...Object.keys(t)])){if(!(n in t)){(!(n in o)||!y(o[n],f[n]))&&(c[n]=f[n]);continue}if(!(n in f)){(!(n in o)||!y(o[n],t[n]))&&(c[n]=t[n]);continue}c[n]=j(o[n],t[n],f[n])}return c}if(g(o)&&g(t)&&g(f)){const c=d((r,u)=>r.find(i=>i.id===u),"find"),n=[];for(const r of f){const u=c(o,r.id),i=c(t,r.id);if(!i){(!u||!y(u,r))&&n.push(r);continue}n.push(u?j(u,i,r):i)}return[...n,...t.filter(r=>!c(f,r.id)&&!c(o,r.id))]}return t}d(j,"merge3");export{j as merge3};
|
package/board/on-host.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var g=Object.defineProperty;var t=(e,r)=>g(e,"name",{value:r,configurable:!0});import w from"node:os";import{execFile as p}from"node:child_process";import{promisify as h}from"node:util";import{tailscaleBin as x}from"../shared/tailscale.mjs";const y=h(p),I=t(e=>String(e||"").trim().replace(/^::ffff:/,""),"bare"),c=t(()=>Object.values(w.networkInterfaces()).flat().map(e=>e.address),"interfaceAddresses");async function u(){const e=x();if(!e)return null;try{const{stdout:r}=await y(e,["status","--json"],{timeout:5e3}),{Self:s,User:n}=JSON.parse(r);return{login:n?.[s.UserID]?.LoginName,addresses:s.TailscaleIPs||[]}}catch{return null}}t(u,"tailnetNode");const f=t(e=>{const r=e.headers["x-forwarded-for"];return I(r?String(r).split(",").at(-1):e.socket.remoteAddress)},"clientOf");async function j(e,r={addresses:c,node:u}){const s=e.headers["x-forwarded-for"],n=f(e);if(r.addresses().includes(n))return!0;const o=e.headers["tailscale-user-login"];if(!s||!o)return!1;const a=await r.node();return!!a&&a.login===o&&a.addresses.includes(n)}t(j,"fromHost");function S(e={addresses:c,node:u},{now:r=Date.now,retryMs:s=5e3}={}){let n=null,o=null,a=-1/0;const m=t(()=>n||r()-a<s?n:(o||=e.node().then(i=>(o=null,i?.login?n=i.login:a=r(),n)),o),"ownerLogin");return async i=>{if(e.addresses().includes(f(i)))return"owner";const l=i.headers["tailscale-user-login"];if(!l)return"other";const d=await m();return d?d===l?"owner":"other":"unread"}}t(S,"ownerGate");const B=S();export{j as fromHost,B as hubGate,S as ownerGate};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var S=Object.defineProperty;var r=(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")}r(f,"settingsFile");const m=r(s=>p.join(s,"data","permission-refusals.json"),"refusalsFile");async function y(s,t,i){let n;try{n=await c.readFile(s,"utf8")}catch(e){if(e.code==="ENOENT")return t;throw e}try{const e=JSON.parse(n);if(e&&typeof e=="object"&&!Array.isArray(e))return e}catch{}throw new d(`${i} isn\u2019t a JSON object, so the hub leaves it as it is. Fix it by hand first.`)}r(y,"readJSON");async function g(s,t){await c.mkdir(p.dirname(s),{recursive:!0});const i=`${s}.${process.pid}.${Date.now()}.tmp`;await c.writeFile(i,JSON.stringify(t,null,2)+`
|
|
2
|
+
`),await c.rename(i,s)}r(g,"writeJSON");const j=r(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(i=>[i.id,t.includes(i.rule)]))}r(x,"readPermissions");async function A(s,t,i,n){const e=u.find(l=>l.id===i);if(!e||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 o=f(s),a=await y(o,{},o);if(a.permissions!==void 0&&(typeof a.permissions!="object"||Array.isArray(a.permissions)||a.permissions===null))throw new d(`${o}\u2019s permissions isn\u2019t an object, so the hub leaves it as it is. Fix it by hand first.`);const w=j(a);w.includes(e.rule)!==n&&(a.permissions={...a.permissions,allow:n?[...w,e.rule]:w.filter(l=>l!==e.rule)},await g(o,a));const h=await b(t);return h[i]&&(delete h[i],await g(m(t),h)),x(s)}r(A,"setPermission");const b=r(s=>y(m(s),{},m(s)),"readRefusals");function O(s){return typeof s!="string"?null:u.find(t=>{const i=/^Bash\((.*)\)$/.exec(t.rule)[1];return new RegExp(`^${i.split("*").map(n=>n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join(".*")}$`).test(s.trim())})||null}r(O,"permissionFor");async function F(s,t,i,n=new Date){if(i?.tool_name!=="Bash")return null;const e=O(i.tool_input?.command);if(!e||!(await x(s))[e.id])return null;const o=await b(t);return o[e.id]={command:i.tool_input.command,how:i.hook_event_name==="PermissionDenied"?"denied":"asked",at:n.toISOString()},await g(m(t),o),e.id}r(F,"recordRefusal");async function B(s,t){const[i,n]=await Promise.all([x(s),b(t)]);return{file:f(s),permissions:u.map(e=>({...e,on:i[e.id],refused:i[e.id]&&n[e.id]||null}))}}r(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,98 +1,30 @@
|
|
|
1
|
-
// New tasks on the board view:
|
|
2
|
-
// pending thread message (docs/adr/
|
|
3
|
-
// docs/adr/
|
|
1
|
+
// New tasks on the board view: bump, edit and delete for a New card or a
|
|
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 { $,
|
|
6
|
-
const {
|
|
5
|
+
const { $, state, toast } = B;
|
|
6
|
+
const { waitOn } = BoardUI;
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
const sheet = $('#sheet');
|
|
10
|
-
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)');
|
|
11
|
-
if (!$('#askForm .row .attach')) $('#askForm .row').insertAdjacentHTML('afterbegin', attachButton());
|
|
12
|
-
// Files picked before the app shell switched away from this board are still attached, so they show again.
|
|
13
|
-
BoardAttach.paint('ask', B.site.root);
|
|
14
|
-
const isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
15
|
-
if (!$('#askForm .send-hint')) $('#askForm button[type=submit]').insertAdjacentHTML('beforebegin', `<span class="send-hint" aria-hidden="true">${isMac ? '⌘↵' : 'Ctrl+↵'}</span>`);
|
|
16
|
-
// ⌘↵ (Ctrl+↵ off a Mac) sends from any composer: the ask, a thread reply, an edit.
|
|
8
|
+
// ⌘↵ (Ctrl+↵ off a Mac) sends a thread reply or an edit from its box; the ask's own is new-task.js's.
|
|
17
9
|
B.on(B.root, 'keydown', (ev) => {
|
|
18
10
|
if (ev.key !== 'Enter' || !(ev.metaKey || ev.ctrlKey) || ev.isComposing) return;
|
|
19
|
-
const form = ev.target instanceof HTMLTextAreaElement ? ev.target.closest('form[data-
|
|
11
|
+
const form = ev.target instanceof HTMLTextAreaElement ? ev.target.closest('form[data-reply], form.edit-form') : null;
|
|
20
12
|
if (!form) return;
|
|
21
13
|
ev.preventDefault();
|
|
22
14
|
form.requestSubmit();
|
|
23
15
|
});
|
|
24
|
-
|
|
25
|
-
B.on($('#askText'), 'input', (e) => { if (!quieted('draft:ask', e.target)) store.set('draft:ask', e.target.value); });
|
|
26
|
-
let stopFitting = null;
|
|
27
|
-
const stopFit = () => { stopFitting?.(); stopFitting = null; };
|
|
28
|
-
B.on(sheet, 'close', stopFit);
|
|
29
|
-
B.atUnmount(stopFit);
|
|
30
|
-
function openSheet() {
|
|
31
|
-
if (sheet.open) return;
|
|
32
|
-
stopFit();
|
|
33
|
-
stopFitting = BoardDialogs.fitSheet(sheet);
|
|
34
|
-
sheet.classList.remove('closing');
|
|
35
|
-
sheet.showModal();
|
|
36
|
-
$('#askText').focus();
|
|
37
|
-
$('#compose').setAttribute('aria-expanded', 'true');
|
|
38
|
-
}
|
|
39
|
-
function closeSheet() {
|
|
40
|
-
if (!sheet.open || sheet.classList.contains('closing')) return;
|
|
41
|
-
$('#compose').setAttribute('aria-expanded', 'false');
|
|
42
|
-
if (reduceMotion.matches) { sheet.close(); return; }
|
|
43
|
-
sheet.classList.add('closing');
|
|
44
|
-
B.later(180, () => { sheet.classList.remove('closing'); sheet.close(); });
|
|
45
|
-
}
|
|
46
|
-
B.on($('#compose'), 'click', openSheet);
|
|
47
|
-
// n pressed twice opens the sheet, as the + button's tooltip says, unless a field or another dialog has focus. With
|
|
48
|
-
// nothing focused the key goes to <body>, outside the view, so this listens on the document. A key switch mode
|
|
49
|
-
// used (sidebar.js) comes defaultPrevented and doesn't count.
|
|
50
|
-
const TWICE_MS = 500;
|
|
51
|
-
let lastN = -Infinity;
|
|
52
|
-
B.on(document, 'keydown', (ev) => {
|
|
53
|
-
if (ev.defaultPrevented) { lastN = -Infinity; return; }
|
|
54
|
-
if (ev.key !== 'n' || ev.metaKey || ev.ctrlKey || ev.altKey || ev.repeat || ev.isComposing) return;
|
|
55
|
-
const field = ev.target instanceof Element && ev.target.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])');
|
|
56
|
-
if (field || document.querySelector('dialog[open]') || B.root.classList.contains('offline')) { lastN = -Infinity; return; }
|
|
57
|
-
if (ev.timeStamp - lastN > TWICE_MS) { lastN = ev.timeStamp; return; }
|
|
58
|
-
lastN = -Infinity;
|
|
59
|
-
ev.preventDefault();
|
|
60
|
-
openSheet();
|
|
61
|
-
});
|
|
62
|
-
B.on($('#sheetClose'), 'click', closeSheet);
|
|
63
|
-
B.on(sheet, 'click', (ev) => { if (ev.target === sheet) closeSheet(); });
|
|
64
|
-
B.on(sheet, 'cancel', (ev) => { ev.preventDefault(); closeSheet(); });
|
|
65
|
-
B.on($('#askForm'), 'submit', async (ev) => {
|
|
66
|
-
ev.preventDefault();
|
|
67
|
-
$('#askText').blur();
|
|
68
|
-
const text = $('#askText').value.trim();
|
|
69
|
-
const files = BoardAttach.files('ask', B.site.root);
|
|
70
|
-
if ((!text && !files.length) || BoardAttach.busy('ask', B.site.root)) return;
|
|
71
|
-
$('#askStatus').textContent = 'Sending…';
|
|
72
|
-
try {
|
|
73
|
-
await sendDraft('draft:ask', $('#askText'), () => $('#askText'), () => B.send('ask', { text, ...(files.length ? { files } : {}) }));
|
|
74
|
-
BoardAttach.clear('ask', B.site.root);
|
|
75
|
-
$('#askStatus').textContent = '';
|
|
76
|
-
closeSheet();
|
|
77
|
-
toast('Sent');
|
|
78
|
-
} catch { $('#askStatus').textContent = 'Could not send'; }
|
|
79
|
-
});
|
|
16
|
+
BoardNewTask.wire(B);
|
|
80
17
|
|
|
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); }
|
|
18
|
+
// A bump moves the New card to the top once the server has recorded it, button (its icon, or the prototype button
|
|
19
|
+
// that bumps it) waiting meanwhile (docs/adr/2026-11-03-1850-a-tap-waits-for-the-server-to-record-it.md).
|
|
20
|
+
async function bumpAsk(id, button) {
|
|
21
|
+
try { await waitOn(button, B.send('ask-bump', { id })); toast('Bumped to the top'); } catch (e) { B.failed(e); }
|
|
89
22
|
}
|
|
90
23
|
B.on(B.root, 'click', (ev) => {
|
|
91
24
|
const bump = ev.target.closest('button[data-bump]');
|
|
92
25
|
if (!bump) return;
|
|
93
26
|
ev.preventDefault();
|
|
94
|
-
bump.
|
|
95
|
-
bumpAsk(bump.dataset.bump);
|
|
27
|
+
bumpAsk(bump.dataset.bump, bump);
|
|
96
28
|
});
|
|
97
29
|
B.bumpAsk = bumpAsk;
|
|
98
30
|
// 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 +66,28 @@ BoardView.part((B) => {
|
|
|
134
66
|
return;
|
|
135
67
|
}
|
|
136
68
|
const cancel = ev.target.closest('.edit-form button[data-edit-cancel]');
|
|
137
|
-
if (cancel) {
|
|
69
|
+
if (cancel) {
|
|
70
|
+
const id = cancel.closest('form').dataset.editForm;
|
|
71
|
+
closeEditor(id);
|
|
72
|
+
BoardAttach.clear('edit:' + id, B.site.root);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
138
75
|
const del = ev.target.closest('button[data-withdraw]');
|
|
139
76
|
if (!del) return;
|
|
140
77
|
ev.preventDefault();
|
|
141
78
|
if (!(await B.confirm('Delete this message? Claude will skip it.'))) return;
|
|
142
79
|
const id = del.dataset.withdraw;
|
|
143
80
|
const { key, msg } = await pendingMessage(del, id);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
B.render();
|
|
147
|
-
if (msg) msg.hidden = true;
|
|
81
|
+
// One request records the withdrawal and hides the message, and the card or message leaves once it has
|
|
82
|
+
// (docs/adr/2026-11-03-1850-a-tap-waits-for-the-server-to-record-it.md).
|
|
148
83
|
try {
|
|
149
|
-
await B.send('ask-withdrawn', { id, ...(key ? { key } : {}) });
|
|
150
|
-
|
|
84
|
+
await waitOn(del, B.send('ask-withdrawn', { id, ...(key ? { key } : {}) }));
|
|
85
|
+
state.edits.delete(id);
|
|
86
|
+
BoardAttach.clear('edit:' + id, B.site.root);
|
|
87
|
+
if (msg) msg.hidden = true;
|
|
151
88
|
toast('Deleted');
|
|
152
89
|
if (key) B.loadThread(key, { force: true });
|
|
153
|
-
} catch
|
|
90
|
+
} catch (e) { B.failed(e); }
|
|
154
91
|
});
|
|
155
92
|
B.on(B.root, 'submit', async (ev) => {
|
|
156
93
|
const form = ev.target.closest('form.edit-form');
|
|
@@ -161,22 +98,21 @@ BoardView.part((B) => {
|
|
|
161
98
|
if (!e) return;
|
|
162
99
|
const text = form.querySelector('textarea').value.trim();
|
|
163
100
|
if (!text) { toast('Write something, or delete the message'); return; }
|
|
164
|
-
|
|
165
|
-
|
|
101
|
+
if (BoardAttach.busy(form.dataset.composer, B.site.root)) return;
|
|
102
|
+
const files = BoardAttach.files(form.dataset.composer, B.site.root);
|
|
103
|
+
// The edit box stays open, its Save waiting, until the server has recorded the edit; then the new text and files
|
|
104
|
+
// show, in the thread and on the New card, and a refusal leaves the box as it was
|
|
105
|
+
// (docs/adr/2026-11-03-1850-a-tap-waits-for-the-server-to-record-it.md).
|
|
106
|
+
e.text = text;
|
|
107
|
+
try {
|
|
108
|
+
await waitOn(form.querySelector('button[type="submit"]'), B.send('ask-edited', { ...e.data, text, ...(files.length ? { files } : {}) }));
|
|
109
|
+
} catch (err) { B.failed(err); return; }
|
|
166
110
|
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)));
|
|
111
|
+
if (before) state.threadCache.set(e.thread, before.map((m) => (m.eventId === id ? { ...m, text, edited: true, files: [...(m.files || []), ...files] } : m)));
|
|
168
112
|
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
|
-
}
|
|
113
|
+
BoardAttach.clear(form.dataset.composer, B.site.root);
|
|
114
|
+
toast('Edited');
|
|
115
|
+
B.loadThread(e.thread, { force: true });
|
|
180
116
|
});
|
|
181
117
|
// The asks watch polls every 10 s while an ask waits for its reply, sooner than the board's own refresh.
|
|
182
118
|
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) {
|