@lovek4d/kickflip 0.4.0 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CONTEXT.md +69 -48
  2. package/README.md +1 -1
  3. package/SKILL.md +22 -20
  4. package/bin/doctor.mjs +4 -4
  5. package/bin/install.mjs +3 -3
  6. package/bin/kickflip.mjs +27 -18
  7. package/bin/wait-events.mjs +2 -2
  8. package/board/agents.mjs +2 -0
  9. package/board/bin/board.mjs +108 -39
  10. package/board/domain.mjs +7 -7
  11. package/board/finish.mjs +4 -3
  12. package/board/headless.mjs +5 -3
  13. package/board/merge3.mjs +1 -0
  14. package/board/on-host.mjs +1 -1
  15. package/board/permissions.mjs +2 -2
  16. package/board/public/app-asks.js +9 -92
  17. package/board/public/app-banner.js +7 -13
  18. package/board/public/app-cards.js +70 -37
  19. package/board/public/app-core.js +24 -25
  20. package/board/public/app-header.js +2 -1
  21. package/board/public/app-lanes.js +55 -23
  22. package/board/public/app-needs.js +129 -4
  23. package/board/public/app-reader.js +174 -0
  24. package/board/public/app-snapshot.js +6 -5
  25. package/board/public/app-threads.js +83 -58
  26. package/board/public/app.html +2 -0
  27. package/board/public/app.js +66 -24
  28. package/board/public/attach.js +2 -2
  29. package/board/public/dialogs.js +9 -39
  30. package/board/public/hub-kit.js +4 -2
  31. package/board/public/hub.css +1 -0
  32. package/board/public/linear-key.js +46 -35
  33. package/board/public/new-task.js +100 -0
  34. package/board/public/prototype-kit.js +28 -3
  35. package/board/public/prototype-switcher.js +42 -107
  36. package/board/public/shell.js +7 -1
  37. package/board/public/sidebar.css +1 -0
  38. package/board/public/sidebar.js +44 -5
  39. package/board/public/style.css +194 -35
  40. package/board/public/ui-cards.js +80 -44
  41. package/board/public/ui-enums.js +1 -1
  42. package/board/public/ui-header.js +2 -2
  43. package/board/public/ui-order.js +12 -10
  44. package/board/public/ui-phase-bars.js +4 -4
  45. package/board/public/ui-questions.js +63 -47
  46. package/board/public/ui-sidebar.js +27 -15
  47. package/board/public/ui-threads.js +27 -14
  48. package/board/public/ui.js +76 -14
  49. package/board/public/view-frame.js +1 -0
  50. package/board/public/view-hub-settings.js +11 -10
  51. package/board/public/view-hub.js +17 -3
  52. package/board/public/view-questions.js +174 -202
  53. package/board/public/view-settings.js +208 -70
  54. package/board/server.mjs +19 -18
  55. package/board/store.mjs +20 -13
  56. package/board/transcript.mjs +2 -0
  57. package/board/worktree.mjs +24 -20
  58. package/hooks-plugin/hooks/board-data-gate.mjs +50 -0
  59. package/hooks-plugin/hooks/hooks.json +9 -0
  60. package/hub/boards.mjs +4 -4
  61. package/hub/front-door.mjs +20 -0
  62. package/hub/github.mjs +3 -3
  63. package/hub/hub.mjs +11 -14
  64. package/hub/linear.mjs +1 -2
  65. package/hub/live.mjs +2 -2
  66. package/hub/locks.mjs +1 -1
  67. package/hub/new-board.mjs +2 -2
  68. package/hub/owner-checks.mjs +1 -0
  69. package/hub/pr-report.mjs +2 -2
  70. package/hub/presence.mjs +1 -1
  71. package/hub/push.mjs +2 -2
  72. package/hub/readiness.mjs +1 -2
  73. package/hub/review-send.mjs +1 -0
  74. package/hub/secrets.mjs +2 -0
  75. package/hub/self-restart.mjs +2 -0
  76. package/hub/sessions.mjs +2 -2
  77. package/hub/share.mjs +1 -0
  78. package/hub/slack.mjs +2 -2
  79. package/hub/stream.mjs +10 -13
  80. package/hub/try.mjs +7 -7
  81. package/hub/upgrade.mjs +1 -1
  82. package/hub/usage.mjs +3 -3
  83. package/hub/views.mjs +1 -1
  84. package/package.json +2 -1
  85. package/shared/audit.mjs +4 -0
  86. package/shared/fake-gh.mjs +17 -4
  87. package/shared/owner-check.mjs +3 -0
  88. package/shared/service.mjs +4 -4
  89. package/shared/tailscale.mjs +1 -0
  90. package/shared/usage-gate.mjs +1 -1
package/board/domain.mjs CHANGED
@@ -1,8 +1,8 @@
1
- var pt=Object.defineProperty;var d=(e,n)=>pt(e,"name",{value:n,configurable:!0});import{routeRecord as A}from"../hub/routes.mjs";import ft from"node:fs";import{ticketBadge as Qe,ticketIn as gt}from"./tickets.mjs";import{reviewLinks as mt,reviewTime as kt,reviewersOf as wt}from"./review.mjs";import{remoteFromInput as ht}from"../hub/repos.mjs";import{reviewsOf as yt,reviewNeeds as St,reviewActions as bt}from"./auto-review.mjs";import{cardPRs as vt,hasPR as Ge,leastReady as Ot,prKeyOf as $t,prURL as xt}from"../shared/card-prs.mjs";import Ve from"node:vm";const _e=Ve.createContext({});for(const e of["ui-enums.js","ui-order.js"])Ve.runInContext(ft.readFileSync(new URL(`./public/${e}`,import.meta.url),"utf8"),_e);const{BUTTON:F,ACTION_KIND:I,BUILDER_KIND:$e,PR_STATE:Z}=_e.BoardUI,{laneOrder:Bt}=_e.BoardUI,At=["item-message","mark-ready","grill-more","prototype-request"],Vn=["planned","waiting","building","fixing","checking","held","merged","done","dropped"],qt=30*6e4,Et="build-refused",Ce=Object.freeze({ask:"builder-ask",answer:"builder-answer"}),It=864e5,pe=d(e=>e.lane==="done"||e.lane==="asks","closedLane"),Mt=d(e=>`wave:${e}`,"waveFold"),Dt=7*It,Xe=d((e,n)=>({id:e,url:n?.workspace?`https://linear.app/${n.workspace}/issue/${e}`:null}),"linearRef"),_t=d((e,n)=>String(e).replace(/^S(\d+)$/,`${n||"INT"}-$1`),"internalRef"),Re=d((e,n,s)=>gt(s,n.ticket)?n.ticket:e.internal===!0?_t(n.id,s?.prefix):n.id,"refOf"),Ct=d((e,n,s,o)=>Qe(e||(n?{source:"internal",prefix:"INT"}:null),s,o)||(n?{id:String(n),url:null,source:null}:null),"ticketOf");function Rt(e,n,s,o,r,a){const p=(e.status||"").toLowerCase();return p==="merged"||p==="done"||p==="dropped"||!r&&(n?.state==="MERGED"||n?.state==="CLOSED")?"done":s||p==="checking"?"progress":o?"review":n?.state==="OPEN"?n.checks?.state==="fail"&&(!n.isDraft||a)?"progress":"review":n?"progress":"todo"}d(Rt,"laneOf");function Nt(e,n){return e?.state==="MERGED"||n.status==="merged"||n.merged_at?Z.merged:e?.state==="CLOSED"?Z.closed:e?.state==="OPEN"?e.isDraft?Z.draft:Z.open:null}d(Nt,"prStateOf");const Ne=d(e=>{const n=(e.status||"").toLowerCase();return n==="dropped"||e.pr?.state==="CLOSED"&&n!=="merged"&&n!=="done"},"droppedCard");function Lt(e){if(!e.some(o=>o.kind==="work")||e.some(o=>o.lane!=="done"))return null;const n=e.filter(o=>o.lane==="done"),s=n.filter(Ne).length;return{merged:n.length-s,dropped:s}}d(Lt,"finishedOf");function Tt(e,n,s){return Object.fromEntries(e.map(o=>{const r=A(n?.reviews,o.github),a=r==="APPROVED"?"approved":r==="CHANGES_REQUESTED"?"changes":Object.hasOwn(s,o.name)?"sent":"unsent";return[o.name,{state:a,since:a==="sent"?A(s,o.name):null,github:o.github||null}]}))}d(Tt,"reviewOf");function Pt(e,n,s,o,r,a){return s?.kind===$e.install?"builder":o==="checking"?"checking":s?"builder":r&&e==="review"?"owner":e==="review"&&n?.state==="OPEN"&&n.isDraft?a?"ci":"ready":e==="review"||e==="stale"?n?.reviewDecision==="APPROVED"?"merge":"reviewers":null}d(Pt,"waitsOnOf");const xe=/(^|\/)prototype-[\w-]+\.html/,jt={reviewers:"Waiting for PR reviewers",merge:"Approved, merges once CI and the merge state allow",ci:"Marked ready for review once CI passes"},Ye={waiting:"Approved, Merge shows once CI and the merge state allow",ready:"Approved and green: tap Merge",merging:"Merging"},Ut="Fix the failing CI",He="Tap Done once it is settled",Wt=["status","agent","ask-link",Ce.answer],ze=d(e=>(e.who==="claude"||e.via==="github")&&!Wt.includes(e.kind),"countsUnread");function Kt({slug:e,wave:n,it:s,order:o,prs:r,repo:a,remote:p,base:u,builder:c,reachedPhase:f,agents:k,worktree:i,eta:h,thread:O,events:w,working:M,said:Q,askedAt:E,topicItems:G,boardReviewers:V,now:P,bumpedAt:l,holdAsk:$,closedAt:S,mergeAt:x,autoMerge:ee,skipDraft:z,tickets:N}){const J=r.map(m=>({repo:m.repo,number:m.number,url:xt(m),...m.gh||{},status:Nt(m.gh,s)})),j=Ot(r.map(m=>m.gh)),C=r[j]?.gh||null,K=J[j]?.status??null,T=!!S&&String(S)>String(s.status_at||"");T&&s.status!=="merged"&&(s={...s,status:"done"});const ge=[T&&S,s.status_at,s.merged_at,...r.map(m=>m.gh?.mergedAt)].filter(Boolean).map(String).sort().pop()||null,L=String(ge||""),D=["done","merged"].includes(s.status)||K===Z.merged&&s.status!=="dropped"?[...Q.filter(m=>m.kind==="item-message").map(m=>m.ts),...E].map(String).filter(m=>m>L).sort().pop():void 0,me=s.owner_step&&!T&&s.status!=="dropped"?String(s.owner_step):null,le=["done","merged"].includes(s.status)||K===Z.merged;D&&(s={...s,status:void 0,next:void 0});const ke=!!D&&Q.some(m=>String(m.ts)>L&&(m.state==="waiting"||m.state==="working")),te=`${e}/${s.id}`,we=wt(n,V),U=Object.fromEntries(Object.entries(mt(n,s,te,G)).map(([m,v])=>[m,kt(v.ts)])),X=Object.values(U).filter(Boolean).sort()[0]||null,he=[...O.map(m=>m.ts),...w.map(m=>m.ts)].sort().pop()||null,Ae=[...O.filter(m=>ze({...m,who:m.who||"claude"})).map(m=>m.ts),...w.map(m=>m.reply?.at)].filter(Boolean).sort(),W=!Ge(s)&&s.pushed?.branch?{branch:s.pushed.branch,at:s.pushed.at||null,host:p?.host||null}:null;let B=Rt(s,C,k.some(m=>m.started&&!m.done&&Y(m)),W,!!D,z);D&&B==="todo"&&(B="progress"),B==="review"&&X&&P-Date.parse(X)>Dt&&(B="stale");const ne=!!me&&le&&B==="done";ne&&(B="review");let R=Pt(B,C,c,(s.status||"").toLowerCase(),W,z);const ie=R==="builder"&&C?.state==="OPEN"&&!C.isDraft,de=B==="progress"&&!R&&C?.state==="OPEN"&&(!C.isDraft||z),se=ne||!!D&&B==="progress"&&!R&&!de&&!ke;se&&(R="owner");const ue=$&&String($.ts)>String(s.held_at||""),ye=B==="todo"&&(ue?!!$.data.hold:s.status==="held"||!!s.held),qe=ye&&(ue?$.data.until:s.pick_up_at)||null,oe=B==="todo"||B==="progress",Se=k.filter(Y).map(m=>String(m.started||"")).filter(Boolean).sort()[0]||"",Ee=l&&!(Se&&String(l)<Se),be=O.filter(m=>(m.who||"claude")==="claude"&&m.kind!=="status").map(m=>String(m.ts)).sort().pop()||"",ce=R==="merge"&&!!x&&String(x)>be,ve=O.filter(m=>m.kind!=="status").sort((m,v)=>String(m.ts).localeCompare(String(v.ts))).at(-1),Ie=B==="todo"&&ve?.kind===Et?ve.text:null,t=D||k.filter(Y).map(m=>m.started).filter(Boolean).map(String).sort().pop()||s.queued_at||["building","fixing","checking"].includes(String(s.status||"").toLowerCase())&&s.status_at||null,g=W&&R==="owner"?`merge ${W.branch} into ${u}`:R==="merge"&&!ee?Ye[ce?"merging":"waiting"]:jt[R],y=K===Z.draft?`Review draft PR #${J[j].number}`:null;return{key:te,kind:"work",order:o,wave:e,waveTitle:n.title||e,id:s.id,ref:Re(n,s,N),title:s.title,status:s.status||null,created:s.created||null,progressSince:t,closedAt:B==="done"?ge:null,internal:n.internal===!0,repo:n.internal===!0&&!n.repo?null:a,lane:B,waitsOn:R,builder:R==="checking"?null:c,reachedPhase:f,agents:k,worktree:i??null,eta:h,working:M,reopened:!!D,awaitsDone:se,ownerStep:se?me:null,queuedAt:B!=="done"&&s.queued_at||null,queued:!1,deps:s.deps||[],held:ye,heldUntil:qe,risky:!!s.risky,bumpable:oe,bumpedAt:oe&&Ee?l:null,holdable:B!=="done",hasNotes:!!(s.plan||String(s.notes||"").trim()),finishable:B!=="done",merging:ce,refusal:Ie,manualSteps:s.manual_steps||[],fromAsk:s.from_ask||null,next:ie||B==="done"||R==="checking"?null:de?Ut:g||s.next||y||null,branch:s.branch||C?.headRefName||W?.branch||null,pushed:B==="done"||ne?null:W,ticket:N?.source==="linear"&&!s.ticket&&s.linear?.length?null:Ct(N,s.ticket,s.ticket||(n.internal?Re(n,s,N):s.linear?.[0]),{repo:a,key:te}),ticketSource:N?.source||null,linear:(s.linear||[]).map(m=>N?.source==="linear"&&Qe(N,m,{key:te,repo:a})||Xe(m,N)),pr:J[j]||null,prs:J,reviewers:we.map(m=>m.name),review:Tt(we,C,U),inReviewSince:X,threadCount:O.length+w.length,lastTs:he,othersAt:Ae,grilling:nt("grill",w,k,E),prototyping:nt("prototype",w,k,E),state:s.state||null,updatedAt:C?.mergedAt||C?.updatedAt||s.merged_at||c?.at||he||null,ask:null,fold:null,action:null,install:B==="done"?null:Ft(s.install,C)}}d(Kt,"workItem");function Ft(e,n){if(!e?.url)return null;const s=e.sha?String(e.sha).toLowerCase():null,o=n?.headRefOid?String(n.headRefOid).toLowerCase():null;return{url:e.url,label:e.label||null,builtAt:e.built_at||null,sha:s,older:!!(s&&o&&!o.startsWith(s))}}d(Ft,"installOf");function Qt(e){if(e?.state!=="OPEN")return[`PR is ${String(e?.state||"unknown").toLowerCase()}`];const{state:n,pending:s,failing:o}=e.checks||{};return[e.isDraft&&"it is a draft",e.reviewDecision!=="APPROVED"&&`review: ${String(e.reviewDecision||"none").toLowerCase().replace(/_/g," ")}`,n==="fail"&&`failing: ${o?.length?o.join(", "):"a check"}`,n==="pending"&&`pending: ${s?H(s,"check"):"checks"}`,e.mergeStateStatus!=="CLEAN"&&`merge state: ${String(e.mergeStateStatus||"unknown").toLowerCase()}`].filter(Boolean)}d(Qt,"mergeBlockers");const Gt=d(e=>!Qt(e).length,"mergeable");function Vt(e,n,s){if(e.held||pe(e))return null;const o=`#item=${encodeURIComponent(e.key)}`;if(Je(e)){const r=n.get(e.questions.first);return xe.test(r?.url||"")?{kind:I.pick,url:`questions.html#${r.id}`,label:"Prototypes ready: pick a take"}:{kind:I.questions,url:`questions.html#${e.questions.first}`,label:`Answer ${H(e.questions.open,"question")}`}}return e.waitsOn==="ready"?{kind:I.ready,url:e.pr.url,label:`Review draft PR #${e.pr.number}`}:e.kind==="ask"&&e.waitsOn==="owner"?{kind:I.done,url:o,label:e.next||He}:e.waitsOn==="merge"&&e.manualSteps.length?{kind:I.steps,url:o,label:"Run the manual steps, then merge"}:e.waitsOn==="merge"&&!s&&!e.merging&&e.prs.every(r=>["MERGED","CLOSED"].includes(r.state)||Gt(r))?{kind:I.merge,url:o,label:"Merge"}:null}d(Vt,"actionOf");const Je=d(e=>!!(e.questions&&e.questions.state!=="resolved"&&e.questions.open>0),"asking"),Ze=new Set([I.pick,I.questions]);function et(e){return pe(e)?null:Je(e)?F.openQuestions:e.action?.kind===I.done?F.markDone:e.kind==="ask"?null:F.grill}d(et,"actionButtonOf");const Xt=d(e=>e.kind==="work"&&e.lane==="todo"&&!e.action&&!e.unpicked,"buildableOf"),Yt=d((e,n)=>/^item:/.test(e.topic||"")&&xe.test(e.url||"")&&!(e.options||[]).some(s=>s.id===n),"picksNoTake");function Ht(e,n){const s=e.filter(a=>a.topic===`item:${n}`),r=[...s.filter(a=>xe.test(a.url||"")).flatMap(a=>a.answers.map(p=>({q:a,a:p,again:Yt(a,p.choice)}))),...s.filter(a=>a.request).map(a=>({q:a,a:a.request,again:!0}))].sort((a,p)=>String(a.a.ts).localeCompare(String(p.a.ts))).at(-1);return r?.again?{id:r.q.id,note:r.a.note.replace(/\s+/g," ").trim()}:null}d(Ht,"noTakePicked");const zt="Prototype again",Jt=Object.freeze({[I.pick]:F.openQuestions,[I.questions]:F.openQuestions,[I.ready]:F.github,[I.steps]:F.github,[I.merge]:F.merge,[I.done]:F.markDone});function Zt(e,n,s){if(e.action=Vt(e,n,s),e.action?.kind===I.questions&&e.questions.fromAsks){Object.assign(e,{action:null,waitsOn:"owner",next:e.action.label,actionButton:et(e),resolvedBy:null});return}e.awaitsDone&&=!e.action,e.awaitsDone||(e.ownerStep=null),e.actionButton=et(e),e.resolvedBy=Jt[e.action?.kind]||null,e.action?.kind===I.merge&&(e.next=Ye.ready),e.awaitsDone&&Object.assign(e,{lane:"review",next:e.ownerStep||He,bumpable:!1,bumpedAt:null});const o=e.lane==="todo"||e.waitsOn==="ready"||e.lane==="progress"&&e.waitsOn==="owner";if(!e.action||!o)return;const r=e.lane==="todo"&&Ze.has(e.action.kind)?"todo":"review";Object.assign(e,{lane:r,waitsOn:e.waitsOn==="ready"?"ready":"owner",next:e.action.label}),r==="review"&&Object.assign(e,{bumpable:!1,bumpedAt:null})}d(Zt,"placeOwnerWait");const tt=d(e=>e.queuedAt||e.agents.some(Y)||["building","fixing"].includes(String(e.status||"").toLowerCase()),"started"),en="none set yet";function Xn(e,n){const s=String(e.status||"").toLowerCase();if(e.next||Ge(e)||e.pushed||e.held||e.queued_at||["held","checking","done","merged","dropped"].includes(s))return!1;const o=n.filter(r=>r?.started);return!o.some(r=>!r.done)&&tt({status:s,agents:o})}d(Xn,"owesNext");function tn(e,n=new Date){const s=new Date(e),o=s.toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"});return s.toDateString()===n.toDateString()?o:`${s.getMonth()+1}/${s.getDate()} ${o}`}d(tn,"askedLabel");function nn(e,n,s,o,r,a,p){const u=n.find(S=>S.kind==="ask"&&S.eventId===e.id),c=u.files,f=(u.text||"").trim().split(`
2
- `)[0]||c.map(S=>S.name).join(", ")||"Ask",k=u.state==="replied"||u.state==="answered",i=r?.state==="progress"&&!(String(a||"")>String(r.at||"")),h=!i&&(!!r||!!a),O=k&&!h&&!i&&p.open>0,w=k&&!h&&!i&&!O&&p.answered>0,M=h||k&&!i&&!O&&!w,Q=k&&!o&&!r?.moved&&[...n.filter(S=>S.who==="claude"&&S.kind!=="ask-link"&&S.kind!=="status").map(S=>S.ts),...n.map(S=>S.reply?.at)].filter(Boolean).sort().pop()||null,E=M||i||O||w,G=n.some(S=>S.who==="love"&&S.kind!=="ask"&&(S.state==="waiting"||S.state==="working")),V=E?"answered":u.state,P=E?null:s||null,l=[...n.filter(ze).map(S=>S.ts),...n.map(S=>S.reply?.at)].filter(Boolean).sort(),$=[...n.map(S=>S.ts),...l,...i?[r.at]:[]].filter(Boolean).sort().pop();return{key:`ask:${e.id}`,kind:"ask",order:0,wave:null,waveTitle:null,id:e.id,ref:E?tn(e.ts):"New",isNew:!E,title:f,status:null,internal:!1,repo:null,lane:i?"progress":M?"asks":"todo",waitsOn:i&&!G?"owner":null,builder:null,agents:[],eta:null,working:n.some(S=>S.state==="working"),deps:[],held:!1,risky:!1,bumpable:!E,bumpedAt:P,manualSteps:[],fromAsk:null,from:null,next:i&&r.next||null,hasNotes:!1,branch:null,finishable:E&&!M,linear:[],pr:null,prs:[],reviewers:[],review:{},inReviewSince:null,progressSince:i&&r.at||null,closedAt:null,threadCount:n.length,lastTs:$,othersAt:E?l:[],grilling:!1,prototyping:!1,created:e.ts,updatedAt:E?$:e.ts,ask:{eventId:e.id,state:V,askedAt:e.ts,bumpedAt:P,edited:!!u.edited,canEdit:!E,text:u.text,files:c,answerAt:Q,from:u.from||null},fold:null,action:null}}d(nn,"askItem");const sn=new Set(["grill","prototype"]),re=d(e=>sn.has(e.kind),"plans"),Y=d(e=>!re(e),"builds"),on={grill:"preheating",prototype:"prototyping"},rn={grill:"grill-more",prototype:"prototype-request"},Le=d((e,n)=>e.kind===rn[n]&&!e.data?.question,"isRequest");function nt(e,n,s,o){if(s.some(a=>a.kind===e&&a.started&&!a.done))return!0;const r=n.filter(a=>Le(a,e)).pop();return!!(r&&!r.reply&&!o.some(a=>String(a)>String(r.ts))&&!s.some(a=>a.kind===e&&a.done&&String(a.started)>String(r.ts)))}d(nt,"planningOf");function Te(e={}){return Object.entries(e).filter(([,n])=>n&&typeof n=="object").map(([n,s])=>({name:n,kind:st(s.kind)||null,model:s.model||(s.kind==="opus"||s.kind==="sonnet"?s.kind:null),label:s.label||null,started:s.started||null,eta:s.done?null:s.eta||null,done:s.done||null,calls:Array.isArray(s.calls)?s.calls:[],verified:s.verified||null})).sort((n,s)=>String(n.started||"").localeCompare(String(s.started||""))||n.name.localeCompare(s.name))}d(Te,"agentsOf");function an(e={}){return Object.values(e).filter(n=>n?.worktree?.path).sort((n,s)=>String(n.started||"").localeCompare(String(s.started||""))).at(-1)?.worktree.path??null}d(an,"worktreeOf");function ln(e,n=[]){const s=e.filter(Y).map(o=>String(o.started||"")).filter(Boolean).sort().pop();return s===void 0?null:n.filter(o=>o.kind==="status"&&o.phase&&!o.planner&&String(o.ts)>=s).sort((o,r)=>String(o.ts).localeCompare(String(r.ts))).pop()?.phase||null}d(ln,"reachedPhaseOf");function dn(e,n=[],s){const o=e.filter(i=>i.started&&!i.done),r=o.every(re),a=r?o:o.filter(Y),p=a.map(i=>String(i.started||"")).sort()[0];if(p===void 0)return null;const u=n.filter(i=>i.kind==="status"&&String(i.ts)>=p&&(r||!i.planner)).sort((i,h)=>String(i.ts).localeCompare(String(h.ts))).pop(),c=a.some(i=>i.kind===$e.install)?$e.install:st(a.at(-1).kind),f=u?.ts||p;return{progress:u?u.phase||u.planner?u.text.replace(": "," \xB7 "):u.text||null:r?on[c]:null,phase:u?.phase||null,at:f,kind:c,planner:r,silentSince:f,stalled:s-Date.parse(f)>=qt}}d(dn,"builderOf");const Pe={opus:"Claude Opus",sonnet:"Claude Sonnet",codex:"Codex"},un=d(({model:e,kind:n})=>Pe[e]||Pe[n]||(n==="claude"?Pe.opus:null),"modelName"),st=d(e=>e==="opus"||e==="sonnet"?"claude":e,"providerOf");function je(e){const n=un(e),{kind:s}=e,o=e.label?.replace(/\bbuilder\b/,"implementer").replace(/\bBuilder\b/,"Implementer"),r=d(p=>p[0].toLowerCase()+p.slice(1),"lower");if(n&&re(e))return`${n} ${s}`;if(n)return`${n} ${r(o?.replace(/^(?:claude\s+)?(?:opus|sonnet|codex)\b\s*/i,"")||"implementer")}`;const a=s?s[0].toUpperCase()+s.slice(1):"";return a?o?o.toLowerCase().includes(s.toLowerCase())?o:`${a} ${r(o)}`:a:o||"Implementer"}d(je,"agentName");const ot=d((e,n)=>!!e.resolved&&(!e.resolved_at||String(e.resolved_at)>=String(n)),"resolvedSince");function cn(e,n,s=[]){const o=n.filter(f=>f.data?.id===e.id),r=o[o.length-1]||null,a=!!(e.resolved&&e.resolved_at&&r&&String(r.ts)>e.resolved_at),p=e.resolved&&!a,u=s.filter(f=>f.data?.question===e.id&&String(f.ts)>String(r?.ts||"")).at(-1),c=p?"resolved":r?r.reply?"replied":"answered":"open";return{...e,state:c,request:u?{id:u.id,ts:u.ts,note:u.data.note||"",choice:u.data.choice||null}:null,answers:o.map(f=>({ts:f.ts,choice:f.data.choice||null,note:f.data.note||"",files:f.data.files||[],reply:f.reply,acted:ot(e,f.ts)}))}}d(cn,"ownerQuestion");const ae=d(e=>e.round??(xe.test(e.url||"")?1:0),"roundOf");function pn(e,n){const s=d((o,r)=>o.confirm_of||(o.options||[]).some(a=>a.id===r.choice),"settles");return e.filter(o=>ae(o)===n&&o.answers.length).map(o=>[o,o.answers.at(-1)]).filter(([o,r])=>s(o,r)).map(([,o])=>o).sort((o,r)=>String(o.ts).localeCompare(String(r.ts))).at(-1)}d(pn,"roundSettledBy");function fn(e){const n=e.find(r=>r.request&&!e.some(a=>ae(a)>ae(r)));if(n)return{n:ae(n)+1,state:"making"};const s=Math.max(0,...e.map(ae));if(!s)return null;const o=pn(e,s);return o?{n:s,state:"closed",take:o.choice}:e.some(r=>ae(r)===s&&r.state!=="resolved")?{n:s,state:"back"}:null}d(fn,"roundState");const fe=d((e,n,s)=>{const o=e.get(n);o?o.push(s):e.set(n,[s])},"add"),Be=d((e,n)=>{const s=new Map;for(const o of e)fe(s,n(o),o);return s},"groupBy");function gn(e,n,s=new Set){const o=new Set(e.map(p=>p.id)),r=[...e];n.some(p=>!o.has(p.topic))&&r.push({id:"",title:"Other"});const a=Be(n,p=>o.has(p.topic)?p.topic:"");return r.map(p=>{const u=a.get(p.id)||[],c=d(i=>u.filter(h=>h.state===i).length,"count"),f=u.every(i=>i.state==="resolved")||s.has(p.id)&&!c("open"),k=u.length&&f?"resolved":"open";return{...p,state:k,questions:u.map(i=>i.id),open:c("open"),answered:c("answered"),replied:c("replied"),resolved:c("resolved"),round:fn(u)}}).filter(p=>p.questions.length)}d(gn,"ownerTopics");const mn={"mark-ready":"Mark ready","grill-more":"grill requested","prototype-request":"prototype requested"},kn="quick build: no grill, as you asked",wn=d((e,n)=>{if(e.kind!=="prototype-request"||!n.has(e.data.question))return e.data.text||A(mn,e.kind)||"";const s=n.get(e.data.question),o=(s.options||[]).find(r=>r.id===e.data.choice);return[`Prototype please:${s.question}`,o&&`${o.id}. ${o.label}`,e.data.note].filter(Boolean).join(`
3
- \u2192 `)},"actionText"),Ue=d(e=>[...e.filter(n=>n.who==="claude"&&!["status","agent",Ce.answer].includes(n.kind)).map(n=>n.ts),...e.filter(n=>n.kind==="ask").map(n=>n.reply?.at)].filter(Boolean).map(String).sort().pop()||"","claudeAtOf");function rt(e,n={}){const s=new Map,o=new Map;for(const k of n.items||[])fe(s,k.topic,k),o.set(k.id,k.topic);const r=new Map,a=new Map,p=new Map,u=new Map,c=new Map,f=new Map;e.forEach((k,i)=>{f.set(k,i),k.data?.key&&At.includes(k.kind)&&fe(r,k.data.key,k),k.kind==="owner-answer"&&o.has(k.data?.id)&&fe(a,o.get(k.data.id),k),k.kind==="ask"&&p.set(k.id,k)});for(const k of e.filter(i=>i.kind==="ask-edited").sort((i,h)=>String(i.ts).localeCompare(String(h.ts)))){u.set(k.data?.id,k.data?.text);for(const i of k.data?.files||[])fe(c,k.data?.id,i)}return{byKey:r,answers:a,asks:p,edits:u,editFiles:c,questions:s,order:f}}d(rt,"threadIndex");const We=d((e,n)=>{const s=n.filter(o=>o?.length);return s.length>1?s.flat().sort((o,r)=>e.order.get(o)-e.order.get(r)):[...s[0]||[]]},"inLogOrder"),hn=/; its log is \S*\/\.claude\/kickflip\/logs\/([^/\s]+\.log)\.$/,yn=d(e=>{const n=!e.log&&typeof e.text=="string"?hn.exec(e.text):null;return n?{...e,text:`${e.text.slice(0,n.index)}.`,log:n[1]}:e},"withLog");function at(e,{lines:n=[],events:s=[],owner:o={},working:r={},fromAsk:a=null,links:p=[],agents:u={},moved:c=[],index:f=rt(s,o)}){const k=e.startsWith("ask:")?[e.slice(4)]:[a,...c].filter(Boolean),i=[...new Set([e,...k.map(l=>`ask:${l}`)])],h=new Map((f.questions.get(`item:${e}`)||[]).map(l=>[l.id,l])),O=f.answers.get(`item:${e}`)||[],w=new Set([...h.values()].map(l=>l.round_of).filter(Boolean)),M=Te(u).filter(Y).map(l=>String(l.started||"")).filter(Boolean),Q=new Set(O.filter(l=>ot(h.get(l.data.id),l.ts)||w.has(l.data.id)||M.some($=>$>String(l.ts))).map(l=>l.id)),E=[...Te(u).flatMap(l=>[...l.started?[{ts:l.started,who:"claude",kind:"agent",text:`${je(l)} started`,finished:!1,eta:l.done?null:l.eta,...re(l)?{planner:!0}:{}}]:[],...l.done?[{ts:l.done,who:"claude",kind:"agent",text:`${je(l)} finished`,finished:!0,eta:null,...re(l)?{planner:!0}:{}}]:[],...l.calls.length?[{ts:l.done||l.calls.at(-1).at,who:"claude",kind:"calls",text:je(l),calls:l.calls.map($=>$.text),verified:l.verified}]:[]]),...n.map(yn).map(l=>({ts:l.ts,who:l.who||"claude",...l.log?{log:l.log}:{},...l.kind==="status"?{kind:"status",...l.planner?{planner:!0}:{}}:{},...Object.values(Ce).includes(l.kind)?{kind:l.kind,ask:l.ask}:{},text:l.text||"",html:l.html||null,link:l.link||null,via:l.via||null})),...We(f,i.map(l=>f.byKey.get(l))).map(l=>({ts:l.ts,who:"love",via:"site",kind:l.kind,text:wn(l,h),files:l.data.files||[],eventId:l.id,reply:l.reply,...Le(l,"grill")||Le(l,"prototype")?{request:!0}:{}})),...O.map(l=>{const $=[l.data.label||l.data.choice,l.data.note].filter(Boolean).join(" \xB7 ");return{ts:l.ts,who:"love",via:"questions",kind:l.kind,text:`${h.get(l.data.id).question}
4
- \u2192 ${$}`,files:l.data.files||[],eventId:l.id,reply:l.reply}}),...We(f,[...new Set(k)].map(l=>f.asks.has(l)?[f.asks.get(l)]:[])).flatMap(l=>[{ts:l.ts,who:"love",via:"ask",kind:l.kind,text:l.data?.text||"",files:l.data?.files||[],eventId:l.id,reply:l.reply,...c.includes(l.id)?{moved:!0}:{},...typeof l.data?.from?.title=="string"?{from:l.data.from.title}:{}},...l.data?.quick===!0?[{ts:l.ts,who:"love",kind:"quick-build",request:!0,text:kn}]:[],...p.map($=>({ts:l.ts,who:"claude",kind:"ask-link",text:`${$.became?"Became":"Added to"} ${$.ref}`,link:`#item=${encodeURIComponent($.key)}`,html:null,via:null}))])].sort((l,$)=>String(l.ts).localeCompare(String($.ts))),G=Ue(E),{edits:V,editFiles:P}=f;for(const l of E){if(!l.eventId)continue;(l.kind==="ask"||l.kind==="item-message")&&typeof V.get(l.eventId)=="string"&&Object.assign(l,{text:V.get(l.eventId),edited:!0,files:[...l.files,...P.get(l.eventId)||[]]});const $=l.kind==="ask"&&(p.length>0||!!l.moved);l.state=l.reply?"replied":$||Q.has(l.eventId)||G>String(l.ts)?"answered":A(r,l.eventId)?"working":"waiting",l.canDelete=l.kind==="item-message"&&(l.state==="working"||l.state==="waiting"),l.canEdit=l.canDelete}return E}d(at,"threadOf");function Sn(e,n,s,o,r,a){const p=d((i,h)=>{const O=a.filter(M=>M.kind==="wave-triage"&&M.data?.[i]===h).pop(),w=r.filter(M=>M.topic===`${i}:${h}`&&(M.state==="open"||M.state==="answered")).length;return{running:!!(O&&!O.reply),questions:w}},"triageOf"),u=new Map(o.map(i=>[i.key,i])),c=d(i=>i.filter(h=>h.lane==="done"&&!Ne(h)).length,"merged"),f=s.map(i=>({l:i,cards:i.keys.map(h=>u.get(h)).filter(Boolean)})).map(i=>({...i,open:!i.l.finished_at&&i.cards.some(h=>h.lane!=="done")})).sort((i,h)=>Number(i.open)-Number(h.open));for(const{l:i,cards:h}of f)for(const O of h)O.lineup={slug:i.slug,title:i.title};const k=f.filter(i=>i.open).map(({l:i,cards:h})=>({kind:"lineup",slug:i.slug,title:i.title,goal:i.goal,keys:h.map(O=>O.key),phases:i.phases.map(O=>O.filter(w=>u.has(w))).filter(O=>O.length),next:bn(h),merged:c(h),total:h.length,triage:p("lineup",i.slug)}));return e.fromWaves?[...k,...n.filter(i=>!i.finished).map(i=>({w:i,cards:o.filter(h=>h.wave===i.slug&&h.kind!=="ask")})).filter(({cards:i})=>i.length).map(({w:i,cards:h})=>({kind:"wave",slug:i.slug,title:i.title,goal:null,keys:h.map(O=>O.key),phases:null,next:null,merged:c(h),total:h.length,triage:p("wave",i.slug)}))]:k}d(Sn,"waveBandsOf");function bn(e){const n=e.filter(a=>a.lane==="todo"),s=n.find(a=>a.ready&&a.buildable)||n[0];if(!s)return null;const o=s.ready&&s.buildable,r=o?"ready":s.held?"held":s.blockedBy.length?`waits on ${s.blockedBy.map(a=>a.ref).join(", ")}`:s.action?"needs you":s.unsorted?"new":s.state||"not ready";return{key:s.key,ref:s.ref,ready:o,word:r}}d(bn,"nextOf");function Yn(e,n,s,o){const r=e.waveBands.find(i=>i.kind==="lineup"&&i.slug===n);if(!r)return null;const a=new Map(e.items.map(i=>[i.key,i.lane])),p=r.keys.filter(i=>a.get(i)==="todo"),u=p.indexOf(s),c=u+o;if(u<0||c<0||c>=p.length)return null;[p[u],p[c]]=[p[c],p[u]];let f=0;const k=r.keys.map(i=>a.get(i)==="todo"?p[f++]:i);return f=0,r.phases.map(i=>k.slice(f,f+=i.length))}d(Yn,"lineupMoved");const vn=d(e=>e.phases||e.items.map(n=>[n]),"lineupPhases");function Hn(e){const{owner:n={},topicItems:s={},events:o=[],threads:r={},prs:a={},working:p={}}=e,u=e.now??Date.now(),c=rt(o,n),f=new Set(o.filter(t=>t.kind==="ask-withdrawn"&&!t.data?.key).map(t=>t.data?.id)),k=new Map(o.filter(t=>t.kind==="ask"&&!f.has(t.id)).map(t=>[t.id,t])),i=d((t,g)=>new Map(o.filter(y=>y.kind===t).sort((y,m)=>String(y.ts).localeCompare(String(m.ts))).map(y=>[y.data?.[g],y.ts])),"latest"),h=i("ask-bump","id"),O=i("card-unbump","key"),w=new Map([...i("card-bump","key")].filter(([t,g])=>!(String(O.get(t)||"")>String(g)))),M=i("card-done","key"),Q=i("card-merge","key"),E=e.autoMerge===!0,G=e.tickets,V=new Map(o.filter(t=>t.kind==="card-hold").sort((t,g)=>String(t.ts).localeCompare(String(g.ts))).map(t=>[t.data?.key,t])),P=new Map,l=e.askLinks||{},$=new Map;for(const[t,g]of Object.entries(l))k.has(t)&&$.set(g,[...$.get(g)||[],t]);const S=[],x=[],ee=new Map,z=new Map,N=new Map,J=new Map,j=e.github?null:ht(e.remote);for(const{slug:t,wave:g,agents:y={}}of e.waves){const m=e.github||g.repo||null;S.push({slug:t,title:g.title||t,status:g.status||null,internal:!!g.internal,finished:null,reviewers:(g.reviewers||[]).map(v=>v.name)}),g.items.forEach((v,q)=>{const _=`${t}/${v.id}`,Ke=($.get(_)||[]).filter(b=>b!==v.from_ask),Me=[v.from_ask,...Ke].filter(b=>k.has(b)),ut=new Set(Me.map(b=>`ask:${b}`)),Fe=[...A(r,_)||[],...Me.flatMap(b=>A(r,`ask:${b}`)||[])];v.from_ask&&P.set(v.from_ask,[...P.get(v.from_ask)||[],{key:_,ref:Re(g,v,G),became:!0}]);const Oe=at(_,{lines:Fe,events:o,owner:n,working:p,fromAsk:v.from_ask||null,moved:Ke,index:c});for(const b of Oe)b.eventId&&ee.set(b.eventId,b.state);z.set(_,Ue(Oe)),x.push(Kt({slug:t,wave:g,it:v,order:q,repo:m,remote:j,base:e.branch||"main",prs:vt(v,m).map(b=>({...b,gh:A(a,$t(b))||null})),...(b=>({agents:b,builder:dn(b,A(r,_),u),reachedPhase:ln(b,A(r,_)),eta:b.map(ct=>ct.eta).filter(Boolean).sort().pop()||null}))(Te(A(y,v.id))),worktree:an(A(y,v.id)),thread:Fe,events:[...We(c,[_,...ut].map(b=>c.byKey.get(b))),...Me.map(b=>k.get(b))],working:Oe.some(b=>b.state==="working"),said:Oe.filter(b=>b.who==="love"&&(b.kind==="item-message"||b.kind==="owner-answer")),askedAt:(c.questions.get(`item:${_}`)||[]).filter(b=>b.asked_at).map(b=>b.asked_at),topicItems:s,boardReviewers:e.reviewers||[],now:u,bumpedAt:w.get(_),holdAsk:V.get(_),closedAt:M.get(_),mergeAt:Q.get(_),autoMerge:E,skipDraft:e.skipDraft===!0,tickets:G}));const De=x[x.length-1];J.set(_,{parent:v.parent?it(t,v.parent):null,related:(v.related||[]).map(b=>it(t,b))}),De.lane==="done"&&N.set(_,De.closedAt||String(De.updatedAt||""))})}const C=new Map(x.map(t=>[t.key,t])),K=new Map,T=e.try||{},ge=e.autoBuild===!0,L=["web","app"].includes(T.kind)?T.kind:e.waves.some(({wave:t})=>t.items.some(g=>g.install?.url))?"app":"web";for(const t of x){if(t.kind!=="work"||t.lane==="done")continue;const g=A(T.cards,t.key)||{},y=g.head||t.pr?.headRefOid,m=L==="web"?g.sha:t.install?.sha,v=t.builder?.kind===$e.install?"running":g.state||(L==="app"&&t.install?"ready":null);t.try={kind:L,state:v,stale:!!(m&&y&&!String(y).toLowerCase().startsWith(String(m).toLowerCase())),url:L==="web"?null:t.install?.url||null,port:L==="web"&&v==="ready"&&g.https||null,sha:m||null,changes:g.ahead>0,script:g.script===!0,autoBuild:ge}}const D=T.main,me=D?.script&&D.sha?{slug:D.slug,branch:D.branch||"main",sha:D.sha.slice(0,7),at:D.at,kind:L,state:D.state||null,url:L==="web"?null:e.main?.install?.url||null,port:L==="web"&&D.state==="ready"&&D.https||null,pull:T.pull?{running:T.pull.running===!0,error:T.pull.error||null}:null}:null,le=e.askProgress||{},ke=i("ask-done","id"),te=Be(o.filter(t=>t.kind==="owner-answer"),t=>t.data?.id),we=Be(o.filter(t=>t.kind==="prototype-request"),t=>t.data?.question),U=(n.items||[]).map(t=>cn(t,te.get(t.id)||[],we.get(t.id)||[])),X=new Map(U.map(t=>[t.id,t])),he=Be(U,t=>t.topic),Ae=d(t=>{const g=he.get(t)||[];return{open:g.filter(y=>y.state==="open").length,answered:g.filter(y=>y.state==="answered").length}},"pendingOn");for(const t of k.values()){const g=`ask:${t.id}`,y=C.get(A(l,t.id)),m=[...P.get(t.id)||[]];if(y&&!m.some(q=>q.key===y.key)&&m.push({key:y.key,ref:y.ref,became:!1}),m.length){K.set(g,m[0].key);continue}if(A(l,t.id)){K.set(g,A(l,t.id));continue}const v=at(g,{lines:A(r,g)||[],events:o,working:p,links:m,index:c});for(const q of v)q.eventId&&ee.set(q.eventId,q.state);z.set(g,Ue(v)),x.push(nn(t,v,h.get(t.id),m.length>0||!!A(l,t.id),A(le,t.id),ke.get(t.id),Ae(`item:${g}`)))}for(const t of S){const g=x.filter(y=>y.wave===t.slug);if(t.finished=Lt(g),t.finished)for(const y of g)y.lane==="done"&&(y.fold=Mt(t.slug))}for(const t of x)t.lineup=null;const W=new Set((n.topics||[]).map(t=>t.id)),B=e.waves.flatMap(({slug:t,wave:g})=>(g.lineups||[]).map(y=>{const m=vn(y).map(v=>v.map(q=>`${t}/${q}`));return{...y,phases:m,keys:m.flat()}})),ne=d(()=>gn([...n.topics||[],...x.filter(t=>!W.has(`item:${t.key}`)).map(t=>({id:`item:${t.key}`,card:t.key,title:`${t.ref}: ${t.title}`,...t.kind==="ask"?{askedAt:t.ask.askedAt}:{},summary:"Questions about this work item."})),...B.filter(t=>!W.has(`lineup:${t.slug}`)).map(t=>({id:`lineup:${t.slug}`,title:t.title,summary:"Wave triage."})),...(e.fromWaves?S:[]).filter(t=>!W.has(`wave:${t.slug}`)).map(t=>({id:`wave:${t.slug}`,title:t.title,summary:"Wave triage."}))],U,new Set(x.filter(pe).map(t=>`item:${t.key}`))),"topicsOf"),R=new Map(ne().map(t=>[t.id,t]));for(const t of x){const g=R.get(`item:${t.key}`);if(!g){t.questions=null;continue}const y=g.questions.filter(q=>X.get(q)?.state!=="resolved"),m=g.questions.find(q=>X.get(q)?.state==="open")||y[0]||g.questions[0],v=g.questions.map(q=>X.get(q)).filter(q=>q?.state==="open");t.questions={topic:g.id,state:g.state,open:g.open,answered:g.answered,replied:g.replied,resolved:g.resolved,first:m,askedAt:v.map(q=>q.asked_at).filter(Boolean).sort(),fromAsks:v.length>0&&v.every(q=>q.ask)}}const ie=new Map(x.map(t=>[t.key,t]));for(const t of x){t.blockedBy=t.kind!=="work"||t.lane==="done"?[]:t.deps.map(y=>ie.get(String(y).includes("/")?y:`${t.wave}/${y}`)).filter(y=>y&&y.lane!=="done"&&!y.reopened).map(lt),Zt(t,X,E),t.lane==="todo"&&t.kind==="work"&&!t.held&&tt(t)&&(t.lane="progress"),t.unpicked=t.kind==="work"&&t.lane==="todo"&&!!Ht(U,t.key),t.unpicked&&(t.state="prototyping"),t.unpicked&&!t.action&&(t.next=zt),t.buildable=Xt(t);const g=t.kind==="work"&&t.lane==="todo";t.unsorted=g&&!!t.fromAsk&&!t.state,t.ready=g&&t.state==="ready"&&!t.blockedBy.length&&!t.action}xn(x,ie,J);const de=new Map((n.items||[]).map(t=>[t.id,t.topic])),se=d(t=>t.kind==="owner-answer"?de.has(t.data?.id)?/^item:(.+)$/.exec(de.get(t.data.id))?.[1]||null:"":t.kind==="ask"?`ask:${t.id}`:t.data?.key||(t.kind.startsWith("ask-")&&t.data?.id?`ask:${t.data.id}`:null),"ownCardOf"),ue=d(t=>{const g=se(t);return K.get(g)||g},"cardOf"),ye=d(t=>{const g=t.startsWith("ask:")?t.slice(4):null,y=g&&A(le,g)?.state==="done"?A(le,g).at:null;return[M.get(t),g&&ke.get(g),y,N.get(t)].filter(Boolean).sort().pop()||""},"closedAt"),qe=d(t=>{const g=ue(t);return g===null?!t.reply:!ie.has(g)||!(ye(g)<String(A(p,t.id)))?!1:ee.has(t.id)?ee.get(t.id)==="working":!t.reply&&!K.has(se(t))&&!(String(z.get(g)||"")>String(t.ts))},"unanswered"),oe=o.filter(t=>A(p,t.id)&&qe(t)).map(t=>({id:t.id,kind:t.kind,key:ue(t),data:t.data,since:A(p,t.id)})),Se=e.autoPull===!0;for(const t of x)t.working=oe.some(g=>g.key===t.key),t.kind==="work"&&(t.lane==="progress"&&t.queuedAt||Se&&t.lane==="todo"&&dt(t))&&Object.assign(t,{lane:"todo",queued:!0}),t.kind==="work"&&t.lane==="progress"&&!t.builder&&!t.next&&t.waitsOn!=="checking"&&!t.working&&!t.queued&&(t.next=en),t.sortAt=(t.queued?t.queuedAt||t.created:Bn[t.lane](t))||t.updatedAt||null;x.sort(Bt);const Ee=Sn(e,S,B,x,U,o),be=ne(),ce=yt({on:e.autoReview,incoming:e.incoming,records:e.reviewRecords,events:o,now:u,refs:new Map(x.filter(t=>t.kind==="work").map(t=>[t.key,t.ref]))}),ve=[...qn(x,be,U),...St(ce)],Ie=x.filter(t=>t.ask?.answerAt).sort((t,g)=>g.ask.answerAt.localeCompare(t.ask.answerAt)).map(t=>({key:t.key,id:t.id,text:t.title,at:t.ask.answerAt}));return On({kind:L,main:me,remote:j&&{host:j.host,name:`${j.host}/${j.path}`},waves:S,waveBands:Ee,items:x,moved:Object.fromEntries(K),answers:Ie,owner:U,ownerTopics:be,reviews:ce,needs:ve,working:oe.length,workingOn:oe,checks:An(e,u)},e)}d(Hn,"buildBoard");function On(e,{listen:n,prompts:s}={}){const o=[...En(s),...e.needs];return{...e,needs:o,needsSummary:In(o),checks:{...e.checks,watcher:{listening:"running",asleep:"asleep",waking:"waking"}[n]||"stopped"}}}d(On,"withHub");const lt=d(e=>({key:e.key,ref:e.ref}),"linkOf"),it=d((e,n)=>String(n).includes("/")?String(n):`${e}/${n}`,"linkKey"),$n=d((e,n)=>e.ref.localeCompare(n.ref,void 0,{numeric:!0}),"byRef");function xn(e,n,s){const o=new Map,r=new Map,a=new Map,p=d((c,f,k)=>c.set(f,[...c.get(f)||[],k]),"add");for(const[c,f]of s){f.parent&&p(o,f.parent,c);for(const k of f.related)k!==c&&(p(r,c,k),p(r,k,c))}for(const c of e)for(const f of c.blockedBy||[])p(a,f.key,c.key);const u=d((c=[])=>[...new Set(c)].map(f=>n.get(f)).filter(Boolean).map(lt).sort($n),"cards");for(const c of e){const f=s.get(c.key)?.parent;c.parent=f?{key:f,ref:n.get(f)?.ref||f.slice(f.indexOf("/")+1)}:null,c.children=u(o.get(c.key)),c.related=u(r.get(c.key)),c.waitsOnIt=u(a.get(c.key))}}d(xn,"linkCards");const Bn={todo:d(e=>e.created,"todo"),progress:d(e=>e.progressSince,"progress"),review:d(e=>e.inReviewSince,"review"),stale:d(e=>e.inReviewSince,"stale"),done:d(e=>e.closedAt,"done"),asks:d(e=>e.ask.answerAt,"asks")},H=d((e,n)=>`${e} ${n}${e===1?"":"s"}`,"plural");function An({checked:e,watcher:n,replyAt:s,threads:o={},working:r={},waves:a=[]},p){const u=Number(e?.every)||30,f=[s,n?.since,...Object.values(r),...Object.values(o).flat().filter(i=>(i.who||"claude")==="claude").map(i=>i.ts),...a.flatMap(i=>Object.values(i.agents||{}).flatMap(h=>Object.values(h||{})).map(h=>h?.done||h?.started))].filter(i=>typeof i=="string"&&!Number.isNaN(Date.parse(i))).reduce((i,h)=>i&&Date.parse(i)>Date.parse(h)?i:h,null),k=!!e?.idle;return{at:e?.at||null,every:k?null:u,idle:k,late:!k&&!!e?.at&&p-Date.parse(e.at)>2*u*6e4,claudeAt:f}}d(An,"checksOf");function qn(e,n,s){const o=new Set(e.map(u=>u.questions?.topic).filter(Boolean)),r=d(u=>u.find(c=>s.find(f=>f.id===c)?.state==="open")||u[0],"firstOpen"),a=n.filter(u=>u.state!=="resolved"&&u.open>0&&!o.has(u.id)).map(u=>{const c=`questions.html#${r(u.questions)}`,f=/^(?:lineup|wave):(.+)$/.exec(u.id)?.[1]||null;return{kind:"Question",title:u.title,text:`${u.title} \xB7 ${H(u.open,"question")}`,url:c,topic:u.id,band:f,questions:{open:u.open,url:c},ready:null}}),p=e.filter(u=>!u.held).map(u=>{const c=u.questions&&u.questions.state!=="resolved"&&u.questions.open>0?{open:u.questions.open,url:`questions.html#${u.questions.first}`}:null,f=u.waitsOn==="ready"?u.pr.url:null,k=!c&&!f&&u.action?{label:u.action.label,url:u.action.url}:null;if(!c&&!f&&!k)return null;const i=`${u.ref}: ${u.title}`,h=[i,c&&H(c.open,"question"),f&&"Ready on GitHub",k?.label].filter(Boolean).join(" \xB7 ");return{kind:c?"Question":f?"Tap Ready":"Action needed",title:i,text:h,url:`#item=${encodeURIComponent(u.key)}`,key:u.key,questions:c,ready:f,action:k}}).filter(Boolean);return[...a,...p]}d(qn,"needsOf");function En(e=[]){return e.map(n=>{const s=n.questions?.length?`Question: ${n.questions.map(o=>o.question).join(" \xB7 ")}`:`${n.tool}: ${n.detail||n.action}`;return{kind:"Prompt",title:s,text:s,url:"",prompt:n}})}d(En,"promptLines");function In(e){const n=e.filter(a=>a.prompt).length,s=e.reduce((a,p)=>a+(p.questions?.open||0),0),o=e.filter(a=>a.ready).length,r=e.filter(a=>a.review).length;return[n&&H(n,"prompt"),s&&H(s,"question"),o&&`${H(o,"draft")} waiting on Ready`,r&&`${H(r,"review")} held`].filter(Boolean).join(" \xB7 ")}d(In,"needsSummary");function zn(e){const n=d((...s)=>e.items.filter(o=>s.includes(o.lane)).length,"count");return{items:e.needs.map(({kind:s,text:o,url:r})=>({kind:s,text:o,url:r})),working:e.working,summary:e.needsSummary,lanes:{review:n("review","stale"),progress:n("progress"),todo:n("todo")},building:e.items.some(s=>(s.agents||[]).some(o=>!o.done))}}d(zn,"needsAnswer");const dt=d(e=>e.buildable&&e.state==="ready"&&!e.held&&!e.unsorted&&!e.blockedBy.length&&!e.questions?.open&&!e.questions?.answered&&!e.grilling&&!e.prototyping,"queueable"),Mn=10;function Dn(e,{cap:n=Mn,pulled:s=[]}={}){const o=e.filter(w=>!pe(w)),r=d(w=>(w.agents||[]).filter(M=>!M.done),"running"),a=o.filter(w=>w.waitsOn==="checking"||r(w).some(Y)).length,p=o.filter(w=>dt(w)&&!w.working).map(w=>w.key),u=o.filter(w=>s.includes(w.key)&&(p.includes(w.key)||w.buildable&&w.working)).map(w=>w.key),c=d(w=>w.queued&&w.queuedAt&&!w.blockedBy.length,"tapped"),f=o.filter(w=>c(w)||p.includes(w.key)&&!u.includes(w.key)).slice(0,Math.max(0,n-a-u.length)),k=f.filter(c).map(w=>w.key),i=f.filter(w=>!c(w)).map(w=>w.key),O=o.some(w=>w.grilling||w.prototyping||r(w).some(re))?[]:o.filter(w=>(w.buildable||w.unpicked&&!w.action)&&w.state!=="ready"&&!w.held&&!w.questions?.open&&!w.questions?.answered&&!w.working).map(w=>w.key);return{idle:!a,pullable:p,waiting:u,starts:k,pulls:i,plannable:O}}d(Dn,"pullOf");function _n(e){return e.filter(n=>n.lane==="todo"&&(!n.queued||n.buildable)||n.lane==="stale").map(n=>JSON.stringify([n.key,n.title,n.deps||[]])).sort().join(`
5
- `)}d(_n,"todoOf");function Jn(e){const n=e.filter(o=>o.kind==="work"&&o.lane!=="done"&&o.questions?.open>0),s=n.map(o=>JSON.stringify([o.key,o.questions.open,o.questions.askedAt||[]])).sort().join(`
6
- `);return{waiting:n.map(o=>o.key),seen:`${s}
1
+ var Ot=Object.defineProperty;var i=(e,t)=>Ot(e,"name",{value:t,configurable:!0});import{routeRecord as $}from"../hub/routes.mjs";import xt from"node:fs";import{ticketBadge as Ze,ticketIn as $t}from"./tickets.mjs";import{reviewLinks as At,reviewTime as Et,reviewersOf as Bt}from"./review.mjs";import{remoteFromInput as Rt}from"../hub/repos.mjs";import{reviewsOf as _t,reviewNeeds as It,reviewActions as qt}from"./auto-review.mjs";import{cardPRs as Mt,hasPR as je,leastReady as Dt,prKeyOf as Ct,prURL as Nt}from"../shared/card-prs.mjs";import et from"node:vm";import{checkWords as Tt,parseCheck as Lt}from"../shared/owner-check.mjs";const Ue=et.createContext({});for(const e of["ui-enums.js","ui-order.js"])et.runInContext(xt.readFileSync(new URL(`./public/${e}`,import.meta.url),"utf8"),Ue);const{BUTTON:Y,ACTION_KIND:M,BUILDER_KIND:De,PR_STATE:re}=Ue.BoardUI,{laneOrder:Pt}=Ue.BoardUI,jt=["item-message","mark-ready","grill-more","prototype-request","reask-request"],fs=["waiting","building","fixing","checking","held","merged","done","dropped"],Ut=10*6e4,Ft=5*6e4,tt="build-refused",be=Object.freeze({ask:"builder-ask",answer:"builder-answer"}),Wt=864e5,z=i(e=>e.lane==="done"||e.lane==="asks","closedLane"),Gt=i(e=>`wave:${e}`,"waveFold"),nt=7*Wt,st=i((e,t)=>({id:e,url:t?.workspace?`https://linear.app/${t.workspace}/issue/${e}`:null}),"linearRef"),Kt=i((e,t)=>String(e).replace(/^S(\d+)$/,`${t||"INT"}-$1`),"internalRef"),Fe=i((e,t,n)=>$t(n,t.ticket)?t.ticket:e.internal===!0?Kt(t.id,n?.prefix):t.id,"refOf"),Qt=i((e,t,n,o)=>Ze(e||(t?{source:"internal",prefix:"INT"}:null),n,o)||(t?{id:String(t),url:null,source:null}:null),"ticketOf");function Vt(e,t,n,o,r,a,u){const p=(e.status||"").toLowerCase();return p==="merged"||p==="done"||p==="dropped"||!r&&(t?.state==="MERGED"||t?.state==="CLOSED")?"done":(n||p==="checking")&&!u?"progress":o?"review":t?.state==="OPEN"?t.checks?.state==="fail"&&(!t.isDraft||a)?"progress":"review":t?"progress":"todo"}i(Vt,"laneOf");function Ht(e,t){return e?.state==="MERGED"||t.status==="merged"||t.merged_at?re.merged:e?.state==="CLOSED"?re.closed:e?.state==="OPEN"?e.isDraft?re.draft:re.open:null}i(Ht,"prStateOf");const We=i(e=>{const t=(e.status||"").toLowerCase();return t==="dropped"||e.pr?.state==="CLOSED"&&t!=="merged"&&t!=="done"},"droppedCard");function Xt(e){if(!e.some(o=>o.kind==="work")||e.some(o=>o.lane!=="done"))return null;const t=e.filter(o=>o.lane==="done"),n=t.filter(We).length;return{merged:t.length-n,dropped:n}}i(Xt,"finishedOf");function Yt(e,t,n){return Object.fromEntries(e.map(o=>{const r=$(t?.reviews,o.github),a=r==="APPROVED"?"approved":r==="CHANGES_REQUESTED"?"changes":Object.hasOwn(n,o.name)?"sent":"unsent";return[o.name,{state:a,since:a==="sent"?$(n,o.name):null,github:o.github||null}]}))}i(Yt,"reviewOf");function zt(e,t,n,o,r,a,{held:u,needsApproval:p}){return n?.kind===De.install?"builder":o==="checking"?"checking":n?"builder":u&&e==="review"?"merge":r&&e==="review"?"owner":e==="review"&&t?.state==="OPEN"&&t.isDraft?a?"ci":"ready":e==="review"||e==="stale"?ot(t,p)?"merge":"reviewers":null}i(zt,"waitsOnOf");const ot=i((e,t)=>e?.reviewDecision==="APPROVED"||!t&&!["REVIEW_REQUIRED","CHANGES_REQUESTED"].includes(e?.reviewDecision),"reviewed"),Ce=/(^|\/)prototype-[\w-]+\.html/,Jt={reviewers:"Waiting for PR reviewers",merge:"Approved, merges once CI and the merge state allow",ci:"Marked ready for review once CI passes"},Ne={waiting:"Approved, Merge shows once CI and the merge state allow",ready:"Approved and green: tap Merge",merging:"Merging"},Ge={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"},Zt=i(e=>`Verified: tap Merge to land it on ${e}`,"heldNext"),en="Fix the failing CI",rt="Tap Done once it is settled",tn=["status","agent","ask-link",be.answer],at=i(e=>(e.who==="claude"||e.via==="github")&&!tn.includes(e.kind),"countsUnread"),nn=/^(?:Not merged yet: |Lander launched headless )/;function sn({slug:e,wave:t,it:n,order:o,prs:r,repo:a,remote:u,base:p,builder:f,reachedPhase:g,agents:c,worktree:l,eta:w,thread:b,events:D,working:q,said:L,askedAt:R,topicItems:I,boardReviewers:G,now:K,bumpedAt:d,holdAsk:x,closedAt:y,mergeAt:A,autoMerge:le,skipDraft:ee,tickets:P}){const te=r.map(k=>({repo:k.repo,number:k.number,url:Nt(k),...k.gh||{},status:Ht(k.gh,n)})),Q=Dt(r.map(k=>k.gh)),C=r[Q]?.gh||null,V=te[Q]?.status??null,W=!!y&&String(y)>String(n.status_at||"");W&&n.status!=="merged"&&(n={...n,status:"done"});const $e=[W&&y,n.status_at,n.merged_at,...r.map(k=>k.gh?.mergedAt)].filter(Boolean).map(String).sort().pop()||null,j=String($e||""),N=["done","merged"].includes(n.status)||V===re.merged&&n.status!=="dropped"?[...L.filter(k=>k.kind==="item-message").map(k=>k.ts),...R].map(String).filter(k=>k>j).sort().pop():void 0,me=n.owner_step&&!W&&n.status!=="dropped"?String(n.owner_step):null,ke=["done","merged"].includes(n.status)||V===re.merged;N&&(n={...n,status:void 0,next:void 0});const Ae=!!N&&L.some(k=>String(k.ts)>j&&(k.state==="waiting"||k.state==="working")),ie=`${e}/${n.id}`,we=Bt(t,G),Ee=Object.fromEntries(Object.entries(At(t,n,ie,I)).map(([k,F])=>[k,Et(F.ts)])),U=Object.values(Ee).filter(Boolean).sort()[0]||null,ne=[...b.map(k=>k.ts),...D.map(k=>k.ts)].sort().pop()||null,Be=[...b.filter(k=>at({...k,who:k.who||"claude"})).map(k=>k.ts),...D.map(k=>k.reply?.at)].filter(Boolean).sort(),X=!je(n)&&n.pushed?.branch?{branch:n.pushed.branch,at:n.pushed.at||null,host:u?.host||null}:null,H=!je(n)&&n.merge_ready?.branch?{branch:n.merge_ready.branch,at:n.merge_ready.at||null,base:p}:null,de=!!n.rebase_run&&C?.state==="OPEN",ce=(n.rebased||[]).at(-1);let E=Vt(n,C,c.some(k=>k.started&&!k.done&&J(k)),X||H,!!N,ee,de);N&&E==="todo"&&(E="progress"),E==="review"&&U&&K-Date.parse(U)>nt&&(E="stale");const se=!!me&&ke&&E==="done";se&&(E="review");let T=zt(E,C,f,(n.status||"").toLowerCase(),X,ee,{held:H,needsApproval:we.length>0});const Re=T==="builder"&&C?.state==="OPEN"&&!C.isDraft,he=E==="progress"&&!T&&C?.state==="OPEN"&&(!C.isDraft||ee),ue=se||!!N&&E==="progress"&&!T&&!he&&!Ae;ue&&(T="owner");const _e=x&&String(x.ts)>String(n.held_at||""),oe=E==="todo"&&(_e?!!x.data.hold:n.status==="held"||!!n.held),Te=oe&&(_e?x.data.until:n.pick_up_at)||null,pe=E==="todo"||E==="progress",ye=c.filter(J).map(k=>String(k.started||"")).filter(Boolean).sort()[0]||"",Ie=d&&!(ye&&String(d)<ye),qe=b.filter(k=>(k.who||"claude")==="claude"&&k.kind!=="status"&&!nn.test(k.text||"")).map(k=>String(k.ts)).sort().pop()||"",fe=[A,H&&n.merge_ready.tapped].filter(Boolean).map(String).sort().pop(),Se=T==="merge"&&!!fe&&fe>qe,s=b.filter(k=>k.kind==="status"&&String(k.ts)>String(fe)).sort((k,F)=>String(k.ts).localeCompare(String(F.ts))).at(-1),m=s?.text.startsWith("Waiting for ")?`${Ne.merging} \xB7 ${s.text[0].toLowerCase()}${s.text.slice(1)}`:Ne.merging,h=b.filter(k=>k.kind!=="status").sort((k,F)=>String(k.ts).localeCompare(String(F.ts))).at(-1),O=E==="todo"&&h?.kind===tt?h.text:null,S=N||c.filter(J).map(k=>k.started).filter(Boolean).map(String).sort().pop()||n.implement_at||["building","fixing","checking"].includes(String(n.status||"").toLowerCase())&&n.status_at||null,_=C?.reviewDecision==="APPROVED",B=X&&T==="owner"?`merge ${X.branch} into ${p}`:H&&T==="merge"?Se?m:Zt(p):T==="merge"&&!le?Se?m:(_?Ne:Ge).waiting:T==="merge"&&!_?Ge.auto:Jt[T],Me=V===re.draft?`Review draft PR #${te[Q].number}`:null,ve=!de&&ce?.to&&ce.to===C?.headRefOid?`Rebased onto ${ce.base||p}, pushed${C.mergeable==="MERGEABLE"?" \xB7 PR is clean again":""}`:null,Le=de&&T==="checking"?c.flatMap(k=>k.calls).sort((k,F)=>String(k.at).localeCompare(String(F.at))).at(-1)?.text??null:null;return{key:ie,kind:"work",order:o,wave:e,waveTitle:t.title||e,id:n.id,ref:Fe(t,n,P),title:n.title,status:n.status||null,created:n.created||null,reviewSince:n.review_since||U||n.merge_ready?.at||n.pushed?.at||n.created||null,progressSince:S,closedAt:E==="done"?$e:null,internal:t.internal===!0,repo:t.internal===!0&&!t.repo?null:a,lane:E,waitsOn:T,builder:T==="checking"?null:f,reachedPhase:g,agents:c,worktree:l??null,eta:w,working:q,reopened:!!N,awaitsDone:ue,ownerStep:ue?me:null,ownerCheck:ue&&me&&n.owner_check?{words:Tt(Lt(n.owner_check.check)),broken:n.owner_check.broken||null}:null,implementAt:E!=="done"&&n.implement_at||null,deps:n.deps||[],held:oe,heldUntil:Te,risky:!!n.risky,bumpable:pe,bumpedAt:pe&&Ie?d:null,holdable:E!=="done",hasNotes:!!(n.plan||String(n.notes||"").trim()),finishable:E!=="done",merging:Se,refusal:O,manualSteps:n.manual_steps||[],fromAsk:n.from_ask||null,unfiled:!!n.unfiled,next:Re||E==="done"||T==="checking"?null:he?en:ve||B||n.next||Me||null,rebaseFailed:Le,branch:n.branch||C?.headRefName||X?.branch||null,pushed:E==="done"||se?null:X,mergeReady:E==="review"?H:null,ticket:P?.source==="linear"&&!n.ticket&&n.linear?.length?null:Qt(P,n.ticket,n.ticket||(t.internal?Fe(t,n,P):n.linear?.[0]),{repo:a,key:ie}),ticketSource:P?.source||null,linear:(n.linear||[]).map(k=>P?.source==="linear"&&Ze(P,k,{key:ie,repo:a})||st(k,P)),pr:te[Q]||null,prs:te,reviewers:we.map(k=>k.name),review:Yt(we,C,Ee),inReviewSince:U,threadCount:b.length+D.length,lastTs:ne,othersAt:Be,grilling:ut("grill",D,c,R),prototyping:ut("prototype",D,c,R),state:n.state||null,updatedAt:C?.mergedAt||C?.updatedAt||n.merged_at||f?.at||ne||null,ask:null,fold:null,action:null,install:E==="done"?null:rn(n.install,C),waitFacts:on(n,{agents:c,thread:b,events:D,refusedAt:O?h.ts:null})}}i(sn,"workItem");function on(e,{agents:t,thread:n,events:o,refusedAt:r}){const a=new Set(n.filter(g=>g.kind===be.answer).map(g=>g.ask)),u=n.filter(g=>g.kind===be.ask&&!a.has(g.ask)).sort((g,c)=>String(g.ts).localeCompare(String(c.ts)))[0],p=i(g=>o.filter(c=>Qe(c,g)).pop()?.ts||null,"requestAt"),f=n.filter(g=>(g.who||"claude")==="claude"&&!["status","agent",tt].includes(g.kind)).map(g=>g.ts);return{statusAt:e.status_at||null,next:e.next||null,ask:u?{at:u.ts,text:u.text}:null,grillAt:p("grill"),prototypeAt:p("prototype"),refusedAt:r,unverified:t.some(g=>g.calls.some(c=>!g.verified||String(c.at)>String(g.verified.at))),writes:[...f,...t.flatMap(g=>[g.verified?.at,g.started])].filter(Boolean).map(String).sort()}}i(on,"waitFactsOf");function rn(e,t){if(!e?.url)return null;const n=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:n,older:!!(n&&o&&!o.startsWith(n))}}i(rn,"installOf");function an(e,{needsApproval:t=!0}={}){if(e?.state!=="OPEN")return[`PR is ${String(e?.state||"unknown").toLowerCase()}`];const{state:n,pending:o,failing:r}=e.checks||{};return[e.isDraft&&"it is a draft",!ot(e,t)&&`review: ${String(e.reviewDecision||"none").toLowerCase().replace(/_/g," ")}`,n==="fail"&&`failing: ${r?.length?r.join(", "):"a check"}`,n==="pending"&&`pending: ${o?Z(o,"check"):"checks"}`,e.mergeStateStatus!=="CLEAN"&&`merge state: ${String(e.mergeStateStatus||"unknown").toLowerCase()}`].filter(Boolean)}i(an,"mergeBlockers");const ln=i((e,t)=>!an(e,{needsApproval:t}).length,"mergeable");function dn(e,t,n){if(z(e))return null;const o=`#item=${encodeURIComponent(e.key)}`;if(lt(e)){const r=t.get(e.questions.first);return Ce.test(r?.url||"")?{kind:M.pick,url:`questions.html#${r.id}`,label:"Prototypes ready: pick a take"}:{kind:M.questions,url:`questions.html#${e.questions.first}`,label:`Answer ${Z(e.questions.open,"question")}`}}return e.waitsOn==="ready"?{kind:M.ready,url:e.pr.url,label:`Review draft PR #${e.pr.number}`}:e.kind==="ask"&&e.waitsOn==="owner"?{kind:M.done,url:o,label:e.next||rt}:e.waitsOn==="merge"&&e.manualSteps.length&&!e.mergeReady?{kind:M.steps,url:o,label:"Run the manual steps, then merge"}:e.waitsOn==="merge"&&(!n||e.mergeReady)&&!e.merging&&e.prs.every(r=>["MERGED","CLOSED"].includes(r.state)||ln(r,e.reviewers.length>0))?{kind:M.merge,url:o,label:"Merge"}:null}i(dn,"actionOf");const lt=i(e=>!!(e.questions&&e.questions.state!=="resolved"&&e.questions.open>0),"asking"),Ke=new Set([M.pick,M.questions]);function it(e){return z(e)?null:lt(e)?Y.openQuestions:e.action?.kind===M.done?Y.markDone:null}i(it,"actionButtonOf");const cn=i(e=>e.kind==="work"&&e.lane==="todo"&&!e.action&&!e.unpicked,"buildableOf"),un=i((e,t)=>/^item:/.test(e.topic||"")&&Ce.test(e.url||"")&&!(e.options||[]).some(n=>n.id===t),"picksNoTake");function pn(e,t){const n=e.filter(a=>a.topic===`item:${t}`),r=[...n.filter(a=>Ce.test(a.url||"")).flatMap(a=>a.answers.map(u=>({q:a,a:u,again:un(a,u.choice)}))),...n.filter(a=>a.request).map(a=>({q:a,a:a.request,again:!0}))].sort((a,u)=>String(a.a.ts).localeCompare(String(u.a.ts))).at(-1);return r?.again?{id:r.q.id,note:r.a.note.replace(/\s+/g," ").trim()}:null}i(pn,"noTakePicked");const fn="Prototype again",gn=Object.freeze({[M.pick]:Y.openQuestions,[M.questions]:Y.openQuestions,[M.ready]:Y.github,[M.steps]:Y.github,[M.merge]:Y.merge,[M.done]:Y.markDone});function mn(e,t,n){if(e.action=dn(e,t,n),e.action?.kind===M.questions&&e.questions.fromAsks){Object.assign(e,{action:null,waitsOn:"owner",next:e.action.label,actionButton:it(e),resolvedBy:null});return}e.awaitsDone&&=!e.action,e.awaitsDone||Object.assign(e,{ownerStep:null,ownerCheck:null}),e.actionButton=it(e),e.resolvedBy=gn[e.action?.kind]||null,e.action?.kind===M.merge&&!e.mergeReady&&(e.next=(e.pr?.reviewDecision==="APPROVED"?Ne:Ge).ready),e.awaitsDone&&Object.assign(e,{lane:"review",next:e.ownerStep||rt,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"&&Ke.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})}i(mn,"placeOwnerWait");const dt=i(e=>e.implementAt||e.agents.some(J)||["building","fixing"].includes(String(e.status||"").toLowerCase()),"started"),ct="none set yet";function gs(e,t){const n=String(e.status||"").toLowerCase();if(e.next||je(e)||e.pushed||e.held||e.implement_at||["held","checking","done","merged","dropped"].includes(n))return!1;const o=t.filter(r=>r?.started);return!o.some(r=>!r.done)&&dt({status:n,agents:o})}i(gs,"owesNext");function kn(e,t=new Date){const n=new Date(e),o=n.toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"});return n.toDateString()===t.toDateString()?o:`${n.getMonth()+1}/${n.getDate()} ${o}`}i(kn,"askedLabel");function wn(e,t,n,o,r,a,u){const p=t.find(y=>y.kind==="ask"&&y.eventId===e.id),f=p.files,g=(p.text||"").trim().split(`
2
+ `)[0]||f.map(y=>y.name).join(", ")||"Ask",c=p.state==="replied"||p.state==="answered",l=r?.state==="progress"&&!(String(a||"")>String(r.at||"")),w=!l&&(!!r||!!a),b=c&&!w&&!l&&u.open>0,D=c&&!w&&!l&&!b&&u.answered>0,q=w||c&&!l&&!b&&!D,L=c&&!o&&!r?.moved&&[...t.filter(y=>y.who==="claude"&&y.kind!=="ask-link"&&y.kind!=="status").map(y=>y.ts),...t.map(y=>y.reply?.at)].filter(Boolean).sort().pop()||null,R=q||l||b||D,I=t.some(y=>y.who==="love"&&y.kind!=="ask"&&(y.state==="waiting"||y.state==="working")),G=R?"answered":p.state,K=R?null:n||null,d=[...t.filter(at).map(y=>y.ts),...t.map(y=>y.reply?.at)].filter(Boolean).sort(),x=[...t.map(y=>y.ts),...d,...l?[r.at]:[]].filter(Boolean).sort().pop();return{key:`ask:${e.id}`,kind:"ask",order:0,wave:null,waveTitle:null,id:e.id,ref:R?kn(e.ts):"New",isNew:!R,title:g,status:null,internal:!1,repo:null,lane:l?"progress":q?"asks":"todo",waitsOn:l&&!I?"owner":null,builder:null,agents:[],eta:null,working:t.some(y=>y.state==="working"),deps:[],held:!1,risky:!1,bumpable:!R,bumpedAt:K,manualSteps:[],fromAsk:null,from:null,next:l&&r.next||null,hasNotes:!1,branch:null,finishable:R&&!q,linear:[],pr:null,prs:[],reviewers:[],review:{},inReviewSince:null,reviewSince:null,progressSince:l&&r.at||null,closedAt:null,threadCount:t.length,lastTs:x,othersAt:R?d:[],grilling:!1,prototyping:!1,created:e.ts,updatedAt:R?x:e.ts,ask:{eventId:e.id,state:G,askedAt:e.ts,bumpedAt:K,edited:!!p.edited,canEdit:!R,text:p.text,files:f,answerAt:L,from:p.from||null},fold:null,action:null}}i(wn,"askItem");const hn=new Set(["grill","prototype"]),ae=i(e=>hn.has(e.kind),"plans"),J=i(e=>!ae(e),"builds"),yn={grill:"grilling",prototype:"prototyping"},Sn={grill:"grill-more",prototype:"prototype-request"},Qe=i((e,t)=>e.kind===Sn[t]&&!e.data?.question,"isRequest");function ut(e,t,n,o){if(n.some(a=>a.kind===e&&a.started&&!a.done))return!0;const r=t.filter(a=>Qe(a,e)).pop();return!!(r&&!r.reply&&!o.some(a=>String(a)>String(r.ts))&&!n.some(a=>a.kind===e&&a.done&&String(a.started)>=String(r.ts)))}i(ut,"planningOf");const ms=30*6e4;function ks(e,t){const n=[];for(const o of e.items){if(z(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&&Qe(a,"prototype")).pop();r&&n.push({key:o.key,request:r.id,since:r.ts});for(const a of e.owner.filter(u=>u.topic===`item:${o.key}`&&u.state==="requested"))n.push({key:o.key,request:a.request.id,question:a.id,since:a.request.ts})}return n}i(ks,"prototypeWaits");function Ve(e={}){return Object.entries(e).filter(([,t])=>t&&typeof t=="object").map(([t,n])=>({name:t,kind:pt(n.kind)||null,model:n.model||(n.kind==="opus"||n.kind==="sonnet"?n.kind:null),label:n.label||null,started:n.started||null,eta:n.done?null:n.eta||null,done:n.done||null,calls:Array.isArray(n.calls)?n.calls:[],verified:n.verified||null})).sort((t,n)=>String(t.started||"").localeCompare(String(n.started||""))||t.name.localeCompare(n.name))}i(Ve,"agentsOf");function He(e={}){return Object.values(e).filter(t=>t?.worktree?.path).sort((t,n)=>String(t.started||"").localeCompare(String(n.started||""))).at(-1)?.worktree.path??null}i(He,"worktreeOf");function vn(e,t=[]){const n=e.filter(J).map(o=>String(o.started||"")).filter(Boolean).sort().pop();return n===void 0?null:t.filter(o=>o.kind==="status"&&o.phase&&!o.planner&&String(o.ts)>=n).sort((o,r)=>String(o.ts).localeCompare(String(r.ts))).pop()?.phase||null}i(vn,"reachedPhaseOf");function bn(e,t=[],n,o=null,r=null,a=!1){const u=e.filter(I=>I.started&&!I.done),p=u.every(ae),f=p?u:u.filter(J),g=f.map(I=>String(I.started||"")).sort()[0];if(g===void 0)return null;const c=t.filter(I=>I.kind==="status"&&String(I.ts)>=g&&(p||!I.planner)).sort((I,G)=>String(I.ts).localeCompare(String(G.ts))).pop(),l=f.some(I=>I.kind===De.install)?De.install:pt(f.at(-1).kind),w=c?.ts||g,b=o!==null&&o>Date.parse(w)?new Date(o).toISOString():w,D=c?c.phase||c.planner?c.text.replace(": "," \xB7 "):c.text||null:p?yn[l]:null,q=n-Date.parse(b),L=p?null:r,R=L===null?q>=Ut:!L;return{progress:D,phase:c?.phase||null,at:w,kind:l,planner:p,silentSince:b,stalled:R,alive:L,quiet:L===!0&&!a&&q>=Ft}}i(bn,"builderOf");const Xe={opus:"Claude Opus",sonnet:"Claude Sonnet",codex:"Codex"},On=i(({model:e,kind:t})=>Xe[e]||Xe[t]||(t==="claude"?Xe.opus:null),"modelName"),pt=i(e=>e==="opus"||e==="sonnet"?"claude":e,"providerOf");function Ye(e){const t=On(e),{kind:n}=e,o=e.label?.replace(/\bbuilder\b/,"implementer").replace(/\bBuilder\b/,"Implementer"),r=i(u=>u[0].toLowerCase()+u.slice(1),"lower");if(t&&ae(e))return`${t} ${n}`;if(t)return`${t} ${r(o?.replace(/^(?:claude\s+)?(?:opus|sonnet|codex)\b\s*/i,"")||"implementer")}`;const a=n?n[0].toUpperCase()+n.slice(1):"";return a?o?o.toLowerCase().includes(n.toLowerCase())?o:`${a} ${r(o)}`:a:o||"Implementer"}i(Ye,"agentName");const ft=i((e,t)=>!!e.resolved&&(!e.resolved_at||String(e.resolved_at)>=String(t)),"resolvedSince");function xn(e,t,n=[],o=[]){const r=t.filter(l=>l.data?.id===e.id),a=r[r.length-1]||null,u=!!(e.resolved&&e.resolved_at&&a&&String(a.ts)>e.resolved_at),p=e.resolved&&!u,f=n.filter(l=>l.data?.question===e.id&&String(l.ts)>String(a?.ts||"")).at(-1),g=p?o.filter(l=>l.data?.question===e.id&&String(l.ts)>String(e.resolved_at||"")).at(-1):null,c=p?"resolved":f?"requested":a?a.reply?"replied":"answered":"open";return{...e,state:c,request:f?{id:f.id,ts:f.ts,note:f.data.note||"",choice:f.data.choice||null}:null,reask:g?{id:g.id,ts:g.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:ft(e,l.ts)}))}}i(xn,"ownerQuestion");const ge=i(e=>e.round??(Ce.test(e.url||"")?1:0),"roundOf");function $n(e,t){const n=i((o,r)=>o.confirm_of||(o.options||[]).some(a=>a.id===r.choice),"settles");return e.filter(o=>ge(o)===t&&o.answers.length).map(o=>[o,o.answers.at(-1)]).filter(([o,r])=>n(o,r)).map(([,o])=>o).sort((o,r)=>String(o.ts).localeCompare(String(r.ts))).at(-1)}i($n,"roundSettledBy");function An(e){const t=e.find(r=>r.request&&!e.some(a=>ge(a)>ge(r)));if(t)return{n:ge(t)+1,state:"making"};const n=Math.max(0,...e.map(ge));if(!n)return null;const o=$n(e,n);return o?{n,state:"closed",take:o.choice}:e.some(r=>ge(r)===n&&r.state!=="resolved")?{n,state:"back"}:null}i(An,"roundState");const Oe=i((e,t,n)=>{const o=e.get(t);o?o.push(n):e.set(t,[n])},"add"),xe=i((e,t)=>{const n=new Map;for(const o of e)Oe(n,t(o),o);return n},"groupBy");function En(e,t,n=new Set){const o=new Set(e.map(u=>u.id)),r=[...e];t.some(u=>!o.has(u.topic))&&r.push({id:"",title:"Other"});const a=xe(t,u=>o.has(u.topic)?u.topic:"");return r.map(u=>{const p=a.get(u.id)||[],f=i(l=>p.filter(w=>w.state===l).length,"count"),g=p.every(l=>l.state==="resolved")||n.has(u.id)&&!f("open"),c=p.length&&g?"resolved":"open";return{...u,state:c,questions:p.map(l=>l.id),open:f("open"),answered:f("answered"),replied:f("replied"),resolved:f("resolved"),round:An(p)}}).filter(u=>u.questions.length)}i(En,"ownerTopics");const gt={"mark-ready":"Mark ready","grill-more":"grill requested","prototype-request":"prototype requested","reask-request":"re-ask requested"},Bn="answers submitted",Rn="quick build: no grill",mt={reply:"Reply",build:"Implement"},kt=i(e=>mt[e.data.intent],"intentLabel"),wt=i(e=>e.kind==="item-message"&&!String(e.data.text||"").trim()&&!e.data.files?.length,"bareMessage"),_n=i((e,t)=>{if(wt(e)&&e.data.intent==="build")return kt(e);if(e.kind!=="prototype-request"||!t.has(e.data.question))return e.data.text||(e.data.files?.length?"":$(gt,e.kind))||"";const n=t.get(e.data.question),o=(n.options||[]).find(r=>r.id===e.data.choice);return[n.question,o&&`${o.id}. ${o.label}`,e.data.note].filter(Boolean).join(`
3
+ \u2192 `)},"actionText"),In=i((e,t)=>e.kind==="item-message"?wt(e)&&e.data.intent==="build"?{request:!0}:mt[e.data.intent]?{status:kt(e)}:{}: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:gt[e.kind]}:{request:!0},"requestOf"),ze=i(e=>[...e.filter(t=>t.who==="claude"&&!["status","agent",be.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 ht(e,t={}){const n=new Map,o=new Map;for(const c of t.items||[])Oe(n,c.topic,c),o.set(c.id,c.topic);const r=new Map,a=new Map,u=new Map,p=new Map,f=new Map,g=new Map;e.forEach((c,l)=>{g.set(c,l),c.data?.key&&jt.includes(c.kind)&&Oe(r,c.data.key,c),c.kind==="owner-answer"&&o.has(c.data?.id)&&Oe(a,o.get(c.data.id),c),c.kind==="ask"&&u.set(c.id,c)});for(const c of e.filter(l=>l.kind==="ask-edited").sort((l,w)=>String(l.ts).localeCompare(String(w.ts)))){p.set(c.data?.id,c.data?.text);for(const l of c.data?.files||[])Oe(f,c.data?.id,l)}return{byKey:r,answers:a,asks:u,edits:p,editFiles:f,questions:n,order:g}}i(ht,"threadIndex");const Je=i((e,t)=>{const n=t.filter(o=>o?.length);return n.length>1?n.flat().sort((o,r)=>e.order.get(o)-e.order.get(r)):[...n[0]||[]]},"inLogOrder"),qn=/; its log is \S*\/\.claude\/kickflip\/logs\/([^/\s]+\.log)\.$/,Mn=i(e=>{const t=!e.log&&typeof e.text=="string"?qn.exec(e.text):null;return t?{...e,text:`${e.text.slice(0,t.index)}.`,log:t[1]}:e},"withLog");function yt(e,{lines:t=[],events:n=[],owner:o={},working:r={},fromAsk:a=null,links:u=[],agents:p={},moved:f=[],index:g=ht(n,o)}){const c=e.startsWith("ask:")?[e.slice(4)]:[a,...f].filter(Boolean),l=[...new Set([e,...c.map(d=>`ask:${d}`)])],w=new Map((g.questions.get(`item:${e}`)||[]).map(d=>[d.id,d])),b=g.answers.get(`item:${e}`)||[],D=new Set([...w.values()].map(d=>d.round_of).filter(Boolean)),q=Ve(p).filter(J).map(d=>String(d.started||"")).filter(Boolean),L=new Set(b.filter(d=>ft(w.get(d.data.id),d.ts)||D.has(d.data.id)||q.some(x=>x>String(d.ts))).map(d=>d.id)),R=[...Ve(p).flatMap(d=>[...d.started?[{ts:d.started,who:"claude",kind:"agent",text:`${Ye(d)} started`,finished:!1,eta:d.done?null:d.eta,...ae(d)?{planner:!0}:{}}]:[],...d.done?[{ts:d.done,who:"claude",kind:"agent",text:`${Ye(d)} finished`,finished:!0,eta:null,...ae(d)?{planner:!0}:{}}]:[],...d.calls.length?[{ts:d.done||d.calls.at(-1).at,who:"claude",kind:"calls",text:Ye(d),calls:d.calls.map(x=>x.text),verified:d.verified}]:[]]),...t.map(Mn).map(d=>({ts:d.ts,who:d.who||"claude",...d.log?{log:d.log}:{},...d.kind==="status"?{kind:"status",...d.planner?{planner:!0}:{}}:{},...Object.values(be).includes(d.kind)?{kind:d.kind,ask:d.ask}:{},text:d.text||"",html:d.html||null,link:d.link||null,via:d.via||null})),...Je(g,l.map(d=>g.byKey.get(d))).map(d=>({ts:d.ts,who:"love",via:"site",kind:d.kind,text:_n(d,w),files:d.data.files||[],eventId:d.id,reply:d.reply,...In(d,w)})),...b.map(d=>{const x=[d.data.label||d.data.choice,d.data.note].filter(Boolean).join(" \xB7 ");return{ts:d.ts,who:"love",via:"questions",kind:d.kind,text:`${w.get(d.data.id).question}
4
+ \u2192 ${x}`,files:d.data.files||[],eventId:d.id,reply:d.reply,status:Bn}}),...Je(g,[...new Set(c)].map(d=>g.asks.has(d)?[g.asks.get(d)]:[])).flatMap(d=>[{ts:d.ts,who:"love",via:"ask",kind:d.kind,text:d.data?.text||"",files:d.data?.files||[],eventId:d.id,reply:d.reply,...f.includes(d.id)?{moved:!0}:{},...typeof d.data?.from?.title=="string"?{from:d.data.from.title}:{}},...d.data?.quick===!0?[{ts:d.ts,who:"love",kind:"quick-build",request:!0,text:Rn}]:[],...u.map(x=>({ts:d.ts,who:"claude",kind:"ask-link",text:x.folded?`Moved to ${x.ref}: ${x.title}`:`${x.became?"Became":"Added to"} ${x.ref}`,link:`#item=${encodeURIComponent(x.key)}`,html:null,via:null}))])].sort((d,x)=>String(d.ts).localeCompare(String(x.ts))),I=ze(R),{edits:G,editFiles:K}=g;for(const d of R){if(!d.eventId)continue;(d.kind==="ask"||d.kind==="item-message")&&typeof G.get(d.eventId)=="string"&&Object.assign(d,{text:G.get(d.eventId),edited:!0,files:[...d.files,...K.get(d.eventId)||[]]});const x=d.kind==="ask"&&(u.length>0||!!d.moved);d.state=d.reply?"replied":x||L.has(d.eventId)||I>String(d.ts)?"answered":$(r,d.eventId)?"working":"waiting",d.canDelete=d.kind==="item-message"&&(d.state==="working"||d.state==="waiting"),d.canEdit=d.canDelete}return R}i(yt,"threadOf");function Dn(e,t,n,o,r,a){const u=i((l,w)=>{const b=a.filter(q=>q.kind==="wave-triage"&&q.data?.[l]===w).pop(),D=r.filter(q=>q.topic===`${l}:${w}`&&(q.state==="open"||q.state==="answered")).length;return{running:!!(b&&!b.reply),questions:D}},"triageOf"),p=new Map(o.map(l=>[l.key,l])),f=i(l=>l.filter(w=>w.lane==="done"&&!We(w)).length,"merged"),g=n.map(l=>({l,cards:l.keys.map(w=>p.get(w)).filter(Boolean)})).map(l=>({...l,open:!l.l.finished_at&&l.cards.some(w=>w.lane!=="done")})).sort((l,w)=>Number(l.open)-Number(w.open));for(const{l,cards:w}of g)for(const b of w)b.lineup={slug:l.slug,title:l.title};const c=g.filter(l=>l.open).map(({l,cards:w})=>({kind:"lineup",slug:l.slug,title:l.title,goal:l.goal,keys:w.map(b=>b.key),phases:l.phases.map(b=>b.filter(D=>p.has(D))).filter(b=>b.length),next:Cn(w),merged:f(w),total:w.length,triage:u("lineup",l.slug)}));return e.fromWaves?[...c,...t.filter(l=>!l.finished).map(l=>({w:l,cards:o.filter(w=>w.wave===l.slug&&w.kind!=="ask")})).filter(({cards:l})=>l.length).map(({w:l,cards:w})=>({kind:"wave",slug:l.slug,title:l.title,goal:null,keys:w.map(b=>b.key),phases:null,next:null,merged:f(w),total:w.length,triage:u("wave",l.slug)}))]:c}i(Dn,"waveBandsOf");function Cn(e){const t=e.filter(a=>a.lane==="todo"),n=t.find(a=>a.ready&&a.buildable)||t[0];if(!n)return null;const o=n.ready&&n.buildable,r=o?"ready":n.held?"held":n.blockedBy.length?`waits on ${n.blockedBy.map(a=>a.ref).join(", ")}`:n.action?"needs you":n.unsorted?"new":n.state||"not ready";return{key:n.key,ref:n.ref,ready:o,word:r}}i(Cn,"nextOf");function ws(e,t,n,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])),u=r.keys.filter(l=>a.get(l)==="todo"),p=u.indexOf(n),f=p+o;if(p<0||f<0||f>=u.length)return null;[u[p],u[f]]=[u[f],u[p]];let g=0;const c=r.keys.map(l=>a.get(l)==="todo"?u[g++]:l);return g=0,r.phases.map(l=>c.slice(g,g+=l.length))}i(ws,"lineupMoved");const Nn=i(e=>e.phases||e.items.map(t=>[t]),"lineupPhases");function hs(e){const{owner:t={},topicItems:n={},events:o=[],threads:r={},prs:a={},working:u={}}=e,p=e.now??Date.now(),f=ht(o,t),g=new Set(o.filter(s=>s.kind==="ask-withdrawn"&&!s.data?.key).map(s=>s.data?.id)),c=new Map(o.filter(s=>s.kind==="ask"&&!g.has(s.id)).map(s=>[s.id,s])),l=i((s,m)=>new Map(o.filter(h=>h.kind===s).sort((h,O)=>String(h.ts).localeCompare(String(O.ts))).map(h=>[h.data?.[m],h.ts])),"latest"),w=l("ask-bump","id"),b=l("card-unbump","key"),D=new Map([...l("card-bump","key")].filter(([s,m])=>!(String(b.get(s)||"")>String(m)))),q=l("card-done","key"),L=l("card-merge","key"),R=e.autoMerge===!0,I=e.tickets,G=new Map(o.filter(s=>s.kind==="card-hold").sort((s,m)=>String(s.ts).localeCompare(String(m.ts))).map(s=>[s.data?.key,s])),K=new Map,d=e.askLinks||{},x=new Map;for(const[s,m]of Object.entries(d))c.has(s)&&x.set(m,[...x.get(m)||[],s]);const y=[],A=[],le=new Map,ee=new Map,P=new Map,te=new Map,Q=e.github?null:Rt(e.remote);for(const{slug:s,wave:m,agents:h={}}of e.waves){const O=e.github||m.repo||null;y.push({slug:s,title:m.title||s,status:m.status||null,internal:!!m.internal,finished:null,reviewers:(m.reviewers||[]).map(S=>S.name)}),m.items.forEach((S,_)=>{const B=`${s}/${S.id}`,Me=(x.get(B)||[]).filter(v=>v!==S.from_ask),ve=[S.from_ask,...Me].filter(v=>c.has(v)),Le=new Set(ve.map(v=>`ask:${v}`)),k=[...$(r,B)||[],...ve.flatMap(v=>$(r,`ask:${v}`)||[])];S.from_ask&&K.set(S.from_ask,[...K.get(S.from_ask)||[],{key:B,ref:Fe(m,S,I),became:!0}]);const F=yt(B,{lines:k,events:o,owner:t,working:u,fromAsk:S.from_ask||null,moved:Me,index:f});for(const v of F)v.eventId&&le.set(v.eventId,v.state);ee.set(B,ze(F)),A.push(sn({slug:s,wave:m,it:S,order:_,repo:O,remote:Q,base:e.branch||"main",prs:Mt(S,O).map(v=>({...v,gh:$(a,Ct(v))||null})),...(v=>({agents:v,builder:bn(v,$(r,B),p,e.moved?.[He($(h,S.id))]??null,$(e.alive,B)??null,!!e.commandRunning?.[He($(h,S.id))]),reachedPhase:vn(v,$(r,B)),eta:v.map(bt=>bt.eta).filter(Boolean).sort().pop()||null}))(Ve($(h,S.id))),worktree:He($(h,S.id))??S.worktree?.path??null,thread:k,events:[...Je(f,[B,...Le].map(v=>f.byKey.get(v))),...ve.map(v=>c.get(v))],working:F.some(v=>v.state==="working"),said:F.filter(v=>v.who==="love"&&(v.kind==="item-message"||v.kind==="owner-answer")),askedAt:(f.questions.get(`item:${B}`)||[]).filter(v=>v.asked_at).map(v=>v.asked_at),topicItems:n,boardReviewers:e.reviewers||[],now:p,bumpedAt:D.get(B),holdAsk:G.get(B),closedAt:q.get(B),mergeAt:L.get(B),autoMerge:R,skipDraft:e.skipDraft===!0,tickets:I}));const Pe=A[A.length-1];te.set(B,{parent:S.parent?vt(s,S.parent):null,related:(S.related||[]).map(v=>vt(s,v))}),Pe.lane==="done"&&P.set(B,Pe.closedAt||String(Pe.updatedAt||""))})}const C=new Map(A.map(s=>[s.key,s])),V=new Map,W=e.try||{},$e=e.autoBuild===!0,j=["web","app"].includes(W.kind)?W.kind:e.waves.some(({wave:s})=>s.items.some(m=>m.install?.url))?"app":"web";for(const s of A){if(s.kind!=="work"||s.lane==="done")continue;const m=$(W.cards,s.key)||{},h=m.head||s.pr?.headRefOid,O=j==="web"?m.sha:s.install?.sha,S=s.builder?.kind===De.install?"running":m.state||(j==="app"&&s.install?"ready":null);s.try={kind:j,state:S,stale:!!(O&&h&&!String(h).toLowerCase().startsWith(String(O).toLowerCase())),url:j==="web"?null:s.install?.url||null,port:j==="web"&&S==="ready"&&m.https||null,sha:O||null,changes:m.ahead>0,script:m.script===!0,autoBuild:$e}}const N=W.main,me=N?.script&&N.sha?{slug:N.slug,branch:N.branch||"main",sha:N.sha.slice(0,7),at:N.at,kind:j,state:N.state||null,url:j==="web"?null:e.main?.install?.url||null,port:j==="web"&&N.state==="ready"&&N.https||null,pull:W.pull?{running:W.pull.running===!0,error:W.pull.error||null}:null}:null,ke=e.askProgress||{},Ae=l("ask-done","id"),ie=xe(o.filter(s=>s.kind==="owner-answer"),s=>s.data?.id),we=xe(o.filter(s=>s.kind==="prototype-request"),s=>s.data?.question),Ee=xe(o.filter(s=>s.kind==="reask-request"),s=>s.data?.question),U=(t.items||[]).map(s=>xn(s,ie.get(s.id)||[],we.get(s.id)||[],Ee.get(s.id)||[])),ne=new Map(U.map(s=>[s.id,s])),Be=xe(U,s=>s.topic),X=i(s=>{const m=Be.get(s)||[];return{open:m.filter(h=>h.state==="open").length,answered:m.filter(h=>h.state==="answered").length}},"pendingOn");for(const s of c.values()){const m=`ask:${s.id}`,h=C.get($(d,s.id)),O=[...K.get(s.id)||[]],S=h&&!O.some(B=>B.key===h.key);if(S&&O.push({key:h.key,ref:h.ref,title:h.title,became:!1,folded:!0}),O.length&&!S){V.set(m,O[0].key);continue}if(!S&&$(d,s.id)){V.set(m,$(d,s.id));continue}const _=yt(m,{lines:$(r,m)||[],events:o,working:u,links:O,index:f});for(const B of _)B.eventId&&le.set(B.eventId,B.state);ee.set(m,ze(_)),A.push(wn(s,_,w.get(s.id),O.length>0||!!$(d,s.id),$(ke,s.id),Ae.get(s.id),X(`item:${m}`)))}for(const s of y){const m=A.filter(h=>h.wave===s.slug);if(s.finished=Xt(m),s.finished)for(const h of m)h.lane==="done"&&(h.fold=Gt(s.slug))}for(const s of A)s.lineup=null;const H=new Set((t.topics||[]).map(s=>s.id)),de=e.waves.flatMap(({slug:s,wave:m})=>(m.lineups||[]).map(h=>{const O=Nn(h).map(S=>S.map(_=>`${s}/${_}`));return{...h,phases:O,keys:O.flat()}})),ce=i(()=>En([...t.topics||[],...A.filter(s=>!H.has(`item:${s.key}`)).map(s=>({id:`item:${s.key}`,card:s.key,title:`${s.ref}: ${s.title}`,...s.kind==="ask"?{askedAt:s.ask.askedAt}:{}})),...de.filter(s=>!H.has(`lineup:${s.slug}`)).map(s=>({id:`lineup:${s.slug}`,title:s.title,summary:"Wave triage."})),...(e.fromWaves?y:[]).filter(s=>!H.has(`wave:${s.slug}`)).map(s=>({id:`wave:${s.slug}`,title:s.title,summary:"Wave triage."}))],U,new Set(A.filter(z).map(s=>`item:${s.key}`))),"topicsOf"),E=new Map(ce().map(s=>[s.id,s]));for(const s of A){const m=E.get(`item:${s.key}`);if(!m){s.questions=null;continue}const h=m.questions.filter(_=>ne.get(_)?.state!=="resolved"),O=m.questions.find(_=>ne.get(_)?.state==="open")||h[0]||m.questions[0],S=m.questions.map(_=>ne.get(_)).filter(_=>_?.state==="open");s.questions={topic:m.id,state:m.state,open:m.open,answered:m.answered,replied:m.replied,resolved:m.resolved,first:O,askedAt:S.map(_=>_.asked_at).filter(Boolean).sort(),fromAsks:S.length>0&&S.every(_=>_.ask)}}const se=new Map(A.map(s=>[s.key,s]));for(const s of A){s.blockedBy=s.kind!=="work"||s.lane==="done"?[]:s.deps.map(h=>se.get(String(h).includes("/")?h:`${s.wave}/${h}`)).filter(h=>h&&h.lane!=="done"&&!h.reopened).map(St),mn(s,ne,R),s.lane==="todo"&&s.kind==="work"&&!s.held&&dt(s)&&(s.lane="progress"),s.unpicked=s.kind==="work"&&s.lane==="todo"&&!!pn(U,s.key),s.unpicked&&(s.state="prototyping"),s.unpicked&&!s.action&&(s.next=fn),s.buildable=cn(s);const m=s.kind==="work"&&s.lane==="todo";s.unsorted=m&&(!!s.fromAsk||s.unfiled)&&!s.state,s.ready=m&&s.state==="ready"&&!s.blockedBy.length&&!s.action}Pn(A,se,te);const T=new Map((t.items||[]).map(s=>[s.id,s.topic])),Re=i(s=>s.kind==="owner-answer"?T.has(s.data?.id)?/^item:(.+)$/.exec(T.get(s.data.id))?.[1]||null:"":s.kind==="ask"?`ask:${s.id}`:s.data?.key||(s.kind.startsWith("ask-")&&s.data?.id?`ask:${s.data.id}`:null),"ownCardOf"),he=i(s=>{const m=Re(s);return V.get(m)||m},"cardOf"),ue=i(s=>{const m=s.startsWith("ask:")?s.slice(4):null,h=m&&$(ke,m)?.state==="done"?$(ke,m).at:null;return[q.get(s),m&&Ae.get(m),h,P.get(s)].filter(Boolean).sort().pop()||""},"closedAt"),_e=i(s=>{const m=he(s);return m===null?!s.reply:!se.has(m)||!(ue(m)<String($(u,s.id)))?!1:le.has(s.id)?le.get(s.id)==="working":!s.reply&&!V.has(Re(s))&&!(String(ee.get(m)||"")>String(s.ts))},"unanswered"),oe=o.filter(s=>$(u,s.id)&&_e(s)).map(s=>({id:s.id,kind:s.kind,key:he(s),data:s.data,since:$(u,s.id)}));for(const s of A)s.working=oe.some(m=>m.key===s.key),s.kind==="work"&&s.lane==="progress"&&!s.builder&&!s.next&&s.waitsOn!=="checking"&&!s.working&&!s.implementAt&&(s.next=ct),s.sortAt=jn[s.lane](s)||(["review","stale"].includes(s.lane)?null:s.updatedAt)||null;for(const s of A){if(s.kind!=="work")continue;const m=(Be.get(`item:${s.key}`)||[]).filter(O=>O.state==="answered").map(O=>O.answers.at(-1).ts).sort().pop(),h=oe.filter(O=>O.key===s.key).map(O=>String(O.since)).sort()[0];s.orchestrator=["progress","todo","stale"].includes(s.lane)||s.waitsOn==="checking"||s.lane==="review"&&s.builder?.stalled?ts(s,s.waitFacts,{answeredAt:m,workingSince:h}):null,delete s.waitFacts}A.sort(Pt);const Te=Dn(e,y,de,A,U,o),pe=ce(),ye=_t({on:e.autoReview,incoming:e.incoming,records:e.reviewRecords,events:o,now:p,refs:new Map(A.filter(s=>s.kind==="work").map(s=>[s.key,s.ref]))}),Ie=[...Fn(A,pe,U),...It(ye)],qe=new Map(Ie.filter(s=>s.questions).map((s,m)=>[s.topic||`item:${s.key}`,m])),fe=i(s=>qe.get(s.id)??qe.size,"rankOf");pe.sort((s,m)=>fe(s)-fe(m));const Se=A.filter(s=>s.ask?.answerAt).sort((s,m)=>m.ask.answerAt.localeCompare(s.ask.answerAt)).map(s=>({key:s.key,id:s.id,text:s.title,at:s.ask.answerAt}));return Tn({kind:j,main:me,remote:Q&&{host:Q.host,name:`${Q.host}/${Q.path}`},waves:y,waveBands:Te,items:A,moved:Object.fromEntries(V),answers:Se,owner:U,ownerTopics:pe,reviews:ye,needs:Ie,working:oe.length,workingOn:oe,checks:Un(e,p)},e)}i(hs,"buildBoard");function Tn(e,{listen:t,prompts:n}={}){const o=[...Gn(n),...e.needs];return{...e,needs:o,needsSummary:Kn(o),checks:{...e.checks,watcher:{listening:"running",asleep:"asleep",waking:"waking"}[t]||"stopped"}}}i(Tn,"withHub");const St=i(e=>({key:e.key,ref:e.ref}),"linkOf"),vt=i((e,t)=>String(t).includes("/")?String(t):`${e}/${t}`,"linkKey"),Ln=i((e,t)=>e.ref.localeCompare(t.ref,void 0,{numeric:!0}),"byRef");function Pn(e,t,n){const o=new Map,r=new Map,a=new Map,u=i((f,g,c)=>f.set(g,[...f.get(g)||[],c]),"add");for(const[f,g]of n){g.parent&&u(o,g.parent,f);for(const c of g.related)c!==f&&(u(r,f,c),u(r,c,f))}for(const f of e)for(const g of f.blockedBy||[])u(a,g.key,f.key);const p=i((f=[])=>[...new Set(f)].map(g=>t.get(g)).filter(Boolean).map(St).sort(Ln),"cards");for(const f of e){const g=n.get(f.key)?.parent;f.parent=g?{key:g,ref:t.get(g)?.ref||g.slice(g.indexOf("/")+1)}:null,f.children=p(o.get(f.key)),f.related=p(r.get(f.key)),f.waitsOnIt=p(a.get(f.key))}}i(Pn,"linkCards");const jn={todo:i(e=>e.created,"todo"),progress:i(e=>e.progressSince,"progress"),review:i(e=>e.reviewSince,"review"),stale:i(e=>e.reviewSince,"stale"),done:i(e=>e.closedAt,"done"),asks:i(e=>e.ask.answerAt,"asks")},Z=i((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural");function Un({checked:e,watcher:t,replyAt:n,threads:o={},working:r={},waves:a=[]},u){const p=Number(e?.every)||30,g=[n,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(w=>Object.values(w||{})).map(w=>w?.done||w?.started))].filter(l=>typeof l=="string"&&!Number.isNaN(Date.parse(l))).reduce((l,w)=>l&&Date.parse(l)>Date.parse(w)?l:w,null),c=!!e?.idle;return{at:e?.at||null,every:c?null:p,idle:c,late:!c&&!!e?.at&&u-Date.parse(e.at)>2*p*6e4,claudeAt:g}}i(Un,"checksOf");function Fn(e,t,n){const o=new Set(e.map(p=>p.questions?.topic).filter(Boolean)),r=i(p=>p.find(f=>n.find(g=>g.id===f)?.state==="open")||p[0],"firstOpen"),a=t.filter(p=>p.state!=="resolved"&&p.open>0&&!o.has(p.id)).map(p=>{const f=`questions.html#${r(p.questions)}`,g=/^(?:lineup|wave):(.+)$/.exec(p.id)?.[1]||null;return{kind:"Question",title:p.title,text:`${p.title} \xB7 ${Z(p.open,"question")}`,url:f,topic:p.id,band:g,questions:{open:p.open,url:f},ready:null,since:n.filter(c=>c.topic===p.id&&c.state==="open").map(c=>c.asked_at).filter(Boolean).sort()[0]||null}}),u=e.map(p=>{const f=p.questions&&p.questions.state!=="resolved"&&p.questions.open>0?{open:p.questions.open,url:`questions.html#${p.questions.first}`}:null,g=p.waitsOn==="ready"?p.pr.url:null,c=!f&&!g&&p.action?{label:p.action.label,url:p.action.url}:null;if(!f&&!g&&!c)return null;const l=`${p.ref}: ${p.title}`,w=[l,f&&Z(f.open,"question"),g&&"Ready on GitHub",c?.label].filter(Boolean).join(" \xB7 ");return{kind:f?"Question":g?"Tap Ready":"Action needed",title:l,text:w,url:`#item=${encodeURIComponent(p.key)}`,key:p.key,questions:f,ready:g,action:c,since:p.created||null}}).filter(Boolean);return[...a,...u].sort(Wn)}i(Fn,"needsOf");const Wn=i((e,t)=>e.since&&t.since?e.since.localeCompare(t.since):!!t.since-!!e.since,"oldestFirst");function Gn(e=[]){return e.map(t=>{const n=t.questions?.length?`Question: ${t.questions.map(o=>o.question).join(" \xB7 ")}`:`${t.tool}: ${t.detail||t.action}`;return{kind:"Prompt",title:n,text:n,url:"",prompt:t}})}i(Gn,"promptLines");function Kn(e){const t=e.filter(a=>a.prompt).length,n=e.reduce((a,u)=>a+(u.questions?.open||0),0),o=e.filter(a=>a.ready).length,r=e.filter(a=>a.review).length;return[t&&Z(t,"prompt"),n&&Z(n,"question"),o&&`${Z(o,"draft")} waiting on Ready`,r&&`${Z(r,"review")} held`].filter(Boolean).join(" \xB7 ")}i(Kn,"needsSummary");function ys(e){const t=i((...n)=>e.items.filter(o=>n.includes(o.lane)).length,"count");return{items:e.needs.map(({kind:n,text:o,url:r})=>({kind:n,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(n=>(n.agents||[]).some(o=>!o.done))}}i(ys,"needsAnswer");const Qn=i(e=>e.buildable&&e.state==="ready"&&!e.held&&!e.unsorted&&!e.blockedBy.length&&!e.questions?.open&&!e.questions?.answered&&!e.grilling&&!e.prototyping,"pullableCard"),Vn=10;function Hn(e,{cap:t=Vn,pulled:n=[]}={}){const o=e.filter(c=>!z(c)),r=i(c=>(c.agents||[]).filter(l=>!l.done),"running"),a=o.filter(c=>c.waitsOn==="checking"||r(c).some(J)).length,u=o.filter(c=>Qn(c)&&!c.working).map(c=>c.key),p=o.filter(c=>n.includes(c.key)&&(u.includes(c.key)||c.buildable&&c.working)).map(c=>c.key),f=o.filter(c=>c.implementAt&&c.state==="ready"&&!c.blockedBy.length).map(c=>c.key),g=u.filter(c=>!p.includes(c)).slice(0,Math.max(0,t-a-p.length-f.length));return{idle:!a,pullable:u,waiting:p,starts:f,pulls:g}}i(Hn,"pullOf");function Ss(e,t,n){return e.filter(o=>o.kind==="work"&&!z(o)&&o.waitsOn!=="checking"&&!(o.agents||[]).some(r=>r.started&&!r.done)).flatMap(o=>{const r=(o.prs||[]).find(u=>u.state==="OPEN"&&u.mergeable==="CONFLICTING"&&u.repo===o.repo&&u.headRefOid),a=r&&`${r.headRefOid}@${n}`;return r&&$(t,o.key)!==a?[{key:o.key,pr:r.number,branch:r.headRefName,conflict:a}]:[]})}i(Ss,"rebasesDue");const Xn=3;function vs(e,{cap:t=Xn}={}){const n=e.filter(a=>!z(a)),o=n.filter(a=>(a.agents||[]).some(u=>ae(u)&&u.started&&!u.done)).length;return n.filter(a=>a.buildable&&a.state!=="ready"&&!a.unsorted&&!a.blockedBy.length&&!a.held&&!a.working&&!a.grilling&&!a.prototyping&&!a.questions&&!(a.agents||[]).some(ae)).slice(0,Math.max(0,t-o)).map(a=>a.key)}i(vs,"arrivalsOf");function Yn(e){return e.filter(t=>t.lane==="todo"||t.lane==="stale").map(t=>JSON.stringify([t.key,t.title,t.deps||[]])).sort().join(`
5
+ `)}i(Yn,"todoOf");function bs(e){const t=e.filter(o=>o.kind==="work"&&o.lane!=="done"&&o.questions?.open>0),n=t.map(o=>JSON.stringify([o.key,o.questions.open,o.questions.askedAt||[]])).sort().join(`
6
+ `);return{waiting:t.map(o=>o.key),seen:`${n}
7
7
  --
8
- ${_n(e)}`}}d(Jn,"triageOf");const Cn=100,Rn=["key","kind","id","ref","title","lane","status","wave","waveTitle","fold","internal","ticket","ticketSource","linear","pr","prs","review","reviewers","risky","sortAt","threadCount","othersAt","hasNotes","questions","next","ask","action","actionButton","builder","working","waitsOn","reachedPhase","grilling","refusal","install","parent","children","related","waitsOnIt","blockedBy","ready","finishable","reopened","manualSteps","branch","worktree"],Zn=d(e=>Object.fromEntries(Rn.filter(n=>n in e).map(n=>[n,e[n]])),"slimDone");function es(e,n=Cn){const s=new Set((e.waveBands||[]).flatMap(r=>r.keys)),o=new Set(e.items.filter(r=>r.lane!=="done").flatMap(r=>[...(r.deps||[]).map(a=>String(a).includes("/")?String(a):`${r.wave}/${a}`),r.parent?.key,...(r.children||[]).map(a=>a.key),...(r.related||[]).map(a=>a.key)]));return e.items.filter(r=>r.lane==="done"&&r.kind==="work").sort((r,a)=>String(a.closedAt||"").localeCompare(String(r.closedAt||""))).slice(n).filter(r=>["done","merged","dropped"].includes(r.status)&&!s.has(r.key)&&!o.has(r.key)&&(!r.questions||r.questions.state==="resolved"))}d(es,"archivedOf");const Nn=d(e=>e.startsWith("ask:")?`ask-${e.slice(4)}`:`item-${e}`,"pushTag");function ts(e){const n=d(a=>e.owner.filter(p=>p.topic===a&&p.state==="open").map(({id:p,question:u})=>({id:p,question:u})),"open"),s=[I.ready,I.merge],o=e.items.filter(a=>a.action).map(a=>{const p=Ze.has(a.action.kind)?n(a.questions.topic):[];return{tag:Nn(a.key),kind:a.action.kind,title:`${a.ref}: ${a.action.label}`,body:p[0]?.question||a.title,url:p.length?a.action.url:`#item=${encodeURIComponent(a.key)}`,now:!s.includes(a.action.kind),questions:p}}),r=e.needs.filter(a=>a.topic).map(a=>{const p=n(a.topic);return{tag:`topic-${a.topic}`,kind:I.questions,title:a.text,body:p[0]?.question||a.title,url:a.url,now:!0,questions:p}});return[...o,...r,...bt(e.reviews)]}d(ts,"ownerActions");function ns(e,n){const s=e.owner.find(a=>a.id===n),o=/^item:([^/:]+\/[^/]+)$/.exec(s?.topic||"")?.[1],r=o&&e.items.find(a=>a.key===o);return!r||r.kind!=="work"||r.lane==="done"||r.held||r.blockedBy.length||r.action||r.unpicked||r.pr||r.pushed||![null,"planned","waiting"].includes(r.status)||r.agents.some(a=>!a.done)||e.owner.some(a=>a.topic===s.topic&&a.state==="open")?null:o}d(ns,"startedByAnswer");function ss(e,{pulled:n=[]}={}){return!e.items.filter(r=>!pe(r)).some(r=>r.waitsOn==="checking"||r.grilling||r.prototyping||(r.agents||[]).some(a=>!a.done))&&!e.working&&!Dn(e.items,{pulled:n}).waiting.length}d(ss,"strictlyIdle");const Ln=15;function os(e,{idle:n,now:s}){const o=s.toISOString(),r=e.last||o;return n?e.since?e.pushed||s-Date.parse(e.since)<Ln*6e4?{state:{...e,last:r},push:!1}:{state:{last:o,since:e.since,pushed:!0},push:!0}:{state:{last:r,since:o},push:!1}:{state:{last:r},push:!1}}d(os,"idleStep");function rs(e,n){const s=d(c=>String(c.closedAt||"")>n,"after"),o=e.filter(c=>c.kind==="work"&&c.lane==="done"&&!Ne(c)&&s(c)),r=new Map;for(const c of e)c.lineup&&r.set(c.lineup.slug,[...r.get(c.lineup.slug)||[],c]);const a=[...r.values()].filter(c=>c.every(f=>f.lane==="done")&&c.some(s)).map(c=>c[0].lineup.title),p=o.slice(0,3).map(c=>c.ref).join(", ")+(o.length>3?` and ${o.length-3} more`:"");return[o.length&&`${o.length} ${o.length===1?"card":"cards"} landed: ${p}`,...a.map(c=>`${c} finished`)].filter(Boolean).join(" \xB7 ")||"Nothing closed since the last idle notification."}d(rs,"idleSummary");export{I as ACTION_KIND,Ce as BUILDER_ASK,$e as BUILDER_KIND,qt as BUILDER_STALLED_AFTER,Et as BUILD_REFUSED,F as BUTTON,Rn as DONE_FIELDS,Cn as DONE_KEPT,Ln as IDLE_MINUTES,At as ITEM_EVENTS,Vn as ITEM_STATUSES,Mn as MAX_IMPLEMENTERS,en as NO_NEXT,on as PLANNER_WORD,zt as PROTOTYPE_AGAIN,Z as PR_STATE,es as archivedOf,Hn as buildBoard,Y as builds,os as idleStep,rs as idleSummary,Yn as lineupMoved,vn as lineupPhases,Qt as mergeBlockers,un as modelName,zn as needsAnswer,Ht as noTakePicked,Xn as owesNext,ts as ownerActions,cn as ownerQuestion,Yt as picksNoTake,re as plans,st as providerOf,Dn as pullOf,Nn as pushTag,Re as refOf,ae as roundOf,pn as roundSettledBy,Zn as slimDone,ns as startedByAnswer,ss as strictlyIdle,rt as threadIndex,at as threadOf,_n as todoOf,Jn as triageOf,Mt as waveFold,On as withHub};
8
+ ${Yn(e)}`}}i(bs,"triageOf");const zn=100,Jn=["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"],Os=i(e=>Object.fromEntries(Jn.filter(t=>t in e).map(t=>[t,e[t]])),"slimDone");function xs(e,t=zn){const n=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)&&!n.has(r.key)&&!o.has(r.key)&&(!r.questions||r.questions.state==="resolved"))}i(xs,"archivedOf");const Zn=i(e=>e.startsWith("ask:")?`ask-${e.slice(4)}`:`item-${e}`,"pushTag");function $s(e){const t=i(a=>e.owner.filter(u=>u.topic===a&&u.state==="open").map(({id:u,question:p})=>({id:u,question:p})),"open"),n=[M.ready,M.merge],o=e.items.filter(a=>a.action).map(a=>{const u=Ke.has(a.action.kind)?t(a.questions.topic):[];return{tag:Zn(a.key),kind:a.action.kind,title:`${a.ref}: ${a.action.label}`,body:u[0]?.question||a.title,url:u.length?a.action.url:`#item=${encodeURIComponent(a.key)}`,now:!n.includes(a.action.kind),questions:u}}),r=e.needs.filter(a=>a.topic).map(a=>{const u=t(a.topic);return{tag:`topic-${a.topic}`,kind:M.questions,title:a.text,body:u[0]?.question||a.title,url:a.url,now:!0,questions:u}});return[...o,...r,...qt(e.reviews)]}i($s,"ownerActions");function As(e,t){const n=e.owner.find(a=>a.id===t),o=/^item:([^/:]+\/[^/]+)$/.exec(n?.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,"waiting"].includes(r.status)||r.agents.some(a=>!a.done)||e.owner.some(a=>a.topic===n.topic&&a.state==="open")?null:o}i(As,"startedByAnswer");function Es(e,{pulled:t=[]}={}){return!e.items.filter(r=>!z(r)).some(r=>r.waitsOn==="checking"||r.grilling||r.prototyping||(r.agents||[]).some(a=>!a.done))&&!e.working&&!Hn(e.items,{pulled:t}).waiting.length}i(Es,"strictlyIdle");const es="Fold the Implement message into the notes, then item set --state ready to start the build";function ts(e,t,{answeredAt:n,workingSince:o}){const r=i(g=>e.agents.some(c=>c.kind===g&&c.started&&!c.done),"running"),a=[e.progressSince,...e.agents.map(g=>g.done)].filter(Boolean).map(String).sort().pop()||null,u=t.ask?{kind:"ask",since:t.ask.at,next:`Answer the builder's ask: ${t.ask.text}`}:n?{kind:"answer",since:n,next:"Act on the owner's answer"}:e.implementAt?e.state==="ready"?null:{kind:"implement",since:e.implementAt,next:es}:Ke.has(e.action?.kind)?null:e.builder?.stalled?{kind:"stalled",since:e.builder.silentSince,next:"Find out why the builder went silent or stopped, then restart or finish its run"}:e.lane==="stale"&&e.inReviewSince?{kind:"stale",since:new Date(Date.parse(e.inReviewSince)+nt).toISOString(),next:"Find out why the review went stale and fix what you can: rebase, CI, comments, a silent reviewer"}: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===ct&&a?{kind:"unowned",since:a,next:"Start a builder on it, or set its Next"}:null;if(!u?.since)return null;const p=String(u.since),f=[t.writes.find(g=>g>p),o].filter(Boolean).sort()[0]||null;return{kind:u.kind,since:p,picked:f,next:u.next}}i(ts,"orchestratorWaitOf");function Bs(e){return e.items.filter(t=>t.orchestrator).map(t=>({key:t.key,ref:t.ref,title:t.title,...t.orchestrator}))}i(Bs,"orchestratorWaits");const Rs=30*6e4,ns=15;function _s(e,{idle:t,now:n}){const o=n.toISOString(),r=e.last||o;return t?e.since?e.pushed||n-Date.parse(e.since)<ns*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}}i(_s,"idleStep");function Is(e,t){const n=i(f=>String(f.closedAt||"")>t,"after"),o=e.filter(f=>f.kind==="work"&&f.lane==="done"&&!We(f)&&n(f)),r=new Map;for(const f of e)f.lineup&&r.set(f.lineup.slug,[...r.get(f.lineup.slug)||[],f]);const a=[...r.values()].filter(f=>f.every(g=>g.lane==="done")&&f.some(n)).map(f=>f[0].lineup.title),u=o.slice(0,3).map(f=>f.ref).join(", ")+(o.length>3?` and ${o.length-3} more`:"");return[o.length&&`${o.length} ${o.length===1?"card":"cards"} landed: ${u}`,...a.map(f=>`${f} finished`)].filter(Boolean).join(" \xB7 ")||"Nothing closed since the last idle notification."}i(Is,"idleSummary");export{M as ACTION_KIND,be as BUILDER_ASK,De as BUILDER_KIND,Ft as BUILDER_QUIET_AFTER,Ut as BUILDER_STALLED_AFTER,tt as BUILD_REFUSED,Y as BUTTON,Jn as DONE_FIELDS,zn as DONE_KEPT,ns as IDLE_MINUTES,jt as ITEM_EVENTS,fs as ITEM_STATUSES,Vn as MAX_IMPLEMENTERS,Xn as MAX_PLANNERS,ct as NO_NEXT,Rs as ORCHESTRATOR_PUSH_AFTER,yn as PLANNER_WORD,fn as PROTOTYPE_AGAIN,ms as PROTOTYPE_WAIT,re as PR_STATE,xs as archivedOf,vs as arrivalsOf,hs as buildBoard,J as builds,_s as idleStep,Is as idleSummary,ws as lineupMoved,Nn as lineupPhases,an as mergeBlockers,On as modelName,ys as needsAnswer,pn as noTakePicked,Bs as orchestratorWaits,gs as owesNext,$s as ownerActions,xn as ownerQuestion,un as picksNoTake,ae as plans,ks as prototypeWaits,pt as providerOf,Hn as pullOf,Zn as pushTag,Ss as rebasesDue,Fe as refOf,ge as roundOf,$n as roundSettledBy,Os as slimDone,As as startedByAnswer,Es as strictlyIdle,ht as threadIndex,yt as threadOf,Yn as todoOf,bs as triageOf,Gt as waveFold,Tn as withHub};
package/board/finish.mjs CHANGED
@@ -1,3 +1,4 @@
1
- var C=Object.defineProperty;var p=(t,e)=>C(t,"name",{value:e,configurable:!0});import R from"node:fs";import w from"node:path";import{spawnSync as v}from"node:child_process";import*as n from"./store.mjs";import{builderRepo as T,git as l,holdingLandLock as x,land as A,passTests as D,worktreesDir as E}from"./worktree.mjs";import{readPermissions as I}from"./permissions.mjs";import{boardAt as L}from"../hub/boards.mjs";import{boardRepo as N}from"../hub/board-repo.mjs";const m=p(t=>{throw new n.BoardError(t)},"fail"),P=p(t=>typeof t=="string"&&t.trim()?t.trim():m("--text must be non-empty text"),"said");async function G(t,e,{text:r,ownerStep:o,repo:i}){const a=e.split("/")[1],d=await n.recordedWorktree(t,e),{repo:f,base:u,remote:$}=T(t,{repo:i??d?.repo}),s={path:d?.path??w.join(E(),a),branch:d?.branch??a},c=await n.cardName(t,e);return $?B(t,e,{...s,base:u,ref:c,text:P(r)}):x(c,async()=>{await n.checkClose(t,e,r,o),await n.checkCallsVerified(t,e),A({...s,repo:w.resolve(f),base:u,ref:c});const h=await n.setItem(t,e,{status:"done",text:r,ownerStep:o});return{said:o===void 0?"landed and done":`landed; waits on the owner step "${o.trim()}"`,lines:[...h.lines,...await q(t,e,f,u)]}})}p(G,"finishCard");async function q(t,e,r,o){if(!(await I(r)).pushMain)return[];const i=l(r,"push","--quiet","origin",o);if(i.ok)return[`pushed ${o} to origin`];const a=`${o} didn't reach origin: ${i.err}; push it with git -C ${r} push origin ${o}`;return await n.appendThread(t,e,{text:`Landed, but ${a}`}),[`landed, but ${a}`]}p(q,"pushBase");async function B(t,e,{path:r,branch:o,base:i,ref:a,text:d}){await n.checkCallsVerified(t,e),R.existsSync(r)||m(`no worktree at ${r}`),l(r,"status","--porcelain").out&&m(`${r} has uncommitted changes; commit them there first`),Number(l(r,"rev-list","--count",`refs/remotes/origin/${i}..HEAD`).out)===0&&m(`${o} has no commits beyond ${i}; nothing to send for review`),D(r,{outcome:"nothing was pushed"});const f=l(r,"push","--quiet","origin",`HEAD:refs/heads/${o}`);f.ok||m(`could not push ${o} to origin: ${f.err}`);const{github:u}=N(L(t).route);if(!u){const b=await n.setItem(t,e,{pushed:o});return await n.appendThread(t,e,{text:d}),{said:`pushed ${o} for its merge`,lines:b.lines}}const{title:$}=await n.briefFacts(t,e),s=v("gh",["pr","create","--draft","--repo",u,"--base",i,"--head",o,"--title",`${a}: ${$}`,"--body",d],{cwd:r,encoding:"utf8"}),c=`${s.stdout||""}
2
- ${s.stderr||""}`,h=(s.status===0||/already exists/.test(c))&&/\/pull\/(\d+)/.exec(c)?.[1];h||m(`could not open a draft PR for ${o}: ${(s.stderr||s.error?.message||`gh exited ${s.status}`).trim().split(`
3
- `)[0]}`);const g=await n.setItem(t,e,{pr:Number(h)});return await n.appendThread(t,e,{text:d}),{said:`draft PR #${h} opened`,lines:g.lines}}p(B,"sendForReview");export{G as finishCard};
1
+ var D=Object.defineProperty;var c=(t,a)=>D(t,"name",{value:a,configurable:!0});import H from"node:fs";import R from"node:path";import{spawnSync as I}from"node:child_process";import*as r from"./store.mjs";import{builderRepo as M,git as f,joiningLand as N,landInTurn as P,passTests as j,syncBase as v,worktreesDir as L}from"./worktree.mjs";import{readPermissions as q}from"./permissions.mjs";import{launchBuilder as F}from"./headless.mjs";import{boardAt as x}from"../hub/boards.mjs";import{boardRepo as A}from"../hub/board-repo.mjs";const T=c(t=>{throw new r.BoardError(t)},"fail"),W=c(t=>typeof t=="string"&&t.trim()?t.trim():T("--text must be non-empty text"),"said");async function B(t,a,{text:e,ownerStep:n,check:o,repo:i,onWait:s}){const h=a.split("/")[1],d=await r.recordedWorktree(t,a),{repo:p,base:l,remote:g}=M(t,{repo:i??d?.repo}),u={path:d?.path??R.join(L(),h),branch:d?.branch??h,repo:R.resolve(p),base:l},m=await r.cardName(t,a),w=await r.readBoardSettings(t),b=g?A(x(t).route).github:null;if(g&&(!b||w.prs))return z(t,a,{...u,ref:m,text:W(e)});const $=await N(m,()=>V(t,a,{tree:u,ref:m,base:l,remote:g,repo:p,settings:w,text:e,ownerStep:n,check:o,onWait:s}));return $.joined?await r.isFinished(t,a)?{said:"landed and done",lines:[]}:B(t,a,{text:e,ownerStep:n,check:o,repo:i,onWait:s}):$}c(B,"finishCard");async function V(t,a,{tree:e,ref:n,base:o,remote:i,repo:s,settings:h,text:d,ownerStep:p,check:l,onWait:g}){await r.checkClose(t,a,d,p,l),await r.checkCallsVerified(t,a),i&&(v(e.repo,o),f(e.repo,"branch","--unset-upstream",e.branch));const u=h.auto_merge||!!(await r.heldMerge(t,a))?.tapped;if(u&&await r.landingLine(t,a),await P({...e,ref:n},{hold:!u,onWait:g,sync:c(()=>i&&v(e.repo,o),"sync")}),!u){const w=await r.setItem(t,a,{mergeReady:e.branch,ownerStep:p,check:l});return await r.appendThread(t,a,{text:d}),{said:"verified; waits for your Merge",lines:w.lines}}const m=await r.setItem(t,a,{status:"done",text:d,ownerStep:p,check:l});return{said:p===void 0?"landed and done":`landed; waits on the owner step "${p.trim()}"`,lines:[...m.lines,...await _(t,a,s,o)]}}c(V,"landCard");async function S(t,a,{launch:e=F}={}){(await r.heldMerge(t,a))?.tapped||T(`${a} isn't waiting on a Merge tap`);try{const{base:o}=M(t,{repo:(await r.recordedWorktree(t,a))?.repo}),i=c(s=>r.appendThread(t,a,{text:`Waiting for ${s} to land`,kind:"status"}),"onWait");return await B(t,a,{text:`Landed on ${o} on your Merge.`,onWait:i})}catch(o){if(!(o instanceof r.BoardError))throw o;const i=o.message.split(`
2
+ `)[0];if(/^rebasing .+ conflicts in |^tests failed in /.test(i)){await r.appendThread(t,a,{text:`Not merged yet: ${i}. A lander fixes it and lands it.`});try{await e(t,{key:a,model:await r.builderModel(t,a)},{lander:!0})}catch(s){await r.appendThread(t,a,{text:`Couldn't launch the lander headless (${s.message}); land it with board.mjs land ${a}.`})}}else await r.appendThread(t,a,{text:`Not merged: ${i}. Tap Merge again.`});throw o}}c(S,"landHeld");async function _(t,a,e,n){if(!(await q(e)).pushMain)return[];const o=f(e,"push","--quiet","origin",n);if(o.ok)return[`pushed ${n} to origin`];const i=`${n} didn't reach origin: ${o.err}; push it with git -C ${e} push origin ${n}`;return await r.appendThread(t,a,{text:`Landed, but ${i}`}),[`landed, but ${i}`]}c(_,"pushBase");async function z(t,a,{path:e,branch:n,base:o,ref:i,text:s}){await r.checkCallsVerified(t,a),H.existsSync(e)||T(`no worktree at ${e}`),f(e,"status","--porcelain").out&&T(`${e} has uncommitted changes; commit them there first`),Number(f(e,"rev-list","--count",`refs/remotes/origin/${o}..HEAD`).out)===0&&T(`${n} has no commits beyond ${o}; nothing to send for review`),await j(e,{outcome:"nothing was pushed"});const{github:h}=A(x(t).route),d=h?await r.cardPR(t,a):null,p=d?f(e,"rev-parse",`refs/remotes/origin/${n}`).out:null,l=d?f(e,"push","--quiet",`--force-with-lease=refs/heads/${n}:${p}`,"origin",`HEAD:refs/heads/${n}`):f(e,"push","--quiet","origin",`HEAD:refs/heads/${n}`);if(l.ok||T(`could not push ${n} to origin: ${l.err}`),d){const $=c(E=>Number(f(e,"rev-list","--count",`refs/remotes/origin/${o}..${E}`).out),"ahead");await r.recordRebase(t,a,{from:p,to:f(e,"rev-parse","HEAD").out,replayed:$("HEAD")<=$(p)});const C=await r.setItem(t,a,{pr:d});return await r.appendThread(t,a,{text:s}),{said:`pushed ${n} to PR #${d}`,lines:C.lines}}if(!h){const $=await r.setItem(t,a,{pushed:n});return await r.appendThread(t,a,{text:s}),{said:`pushed ${n} for its merge`,lines:$.lines}}const{title:g}=await r.briefFacts(t,a),u=I("gh",["pr","create","--draft","--repo",h,"--base",o,"--head",n,"--title",`${i}: ${g}`,"--body",s],{cwd:e,encoding:"utf8"}),m=`${u.stdout||""}
3
+ ${u.stderr||""}`,w=(u.status===0||/already exists/.test(m))&&/\/pull\/(\d+)/.exec(m)?.[1];w||T(`could not open a draft PR for ${n}: ${(u.stderr||u.error?.message||`gh exited ${u.status}`).trim().split(`
4
+ `)[0]}`);const b=await r.setItem(t,a,{pr:Number(w)});return await r.appendThread(t,a,{text:s}),{said:`draft PR #${w} opened`,lines:b.lines}}c(z,"sendForReview");export{B as finishCard,S as landHeld};
@@ -1,3 +1,5 @@
1
- var $=Object.defineProperty;var d=(e,r)=>$(e,"name",{value:r,configurable:!0});import u from"node:fs";import g from"node:os";import h from"node:path";import{spawn as w}from"node:child_process";import{fileURLToPath as v}from"node:url";import{claudeBin as R,inherited as k}from"../hub/sessions.mjs";import{boardAt as y}from"../hub/boards.mjs";import{readHubRules as x}from"../hub/rules.mjs";import{builderRepo as j}from"./worktree.mjs";import{appendThread as T,briefFacts as B,readBoardRules as S}from"./store.mjs";import{modelName as A}from"./domain.mjs";const I=v(new URL("./bin/board.mjs",import.meta.url)),m=d(e=>`node ${I} --site ${e}`,"cli"),O=d(e=>`git -c core.commentChar=';' rebase ${e}`,"rebase"),W=d(e=>e.length?["Rules from the hub and this board:",...e.map(r=>`- ${r}`),""]:[],"ruleLines"),f=d(({route:e,key:r,worktree:c,base:o})=>[`- With no unverified calls, done finishes the card itself: it rebases onto ${o}, reruns the full test suite, fast-forwards ${o} and closes the card (on a board with pull requests it pushes the branch and opens a draft PR instead).`,`- On a conflict it undoes its rebase and names the files: run \`${O(o)}\` in ${c}, resolve each conflict keeping both sides' intent, continue with \`git -c core.commentChar=';' rebase --continue\`, rerun the tests, and run done again.`,"- On failing tests, fix them, commit, and run done again.","- While another card is landing it refuses, naming that card: wait a minute and run done again.",`- Finishing never goes to the orchestrator. If done refuses for a reason you can't fix, record it with \`${m(e)} builder call ${r} "<why>"\` and run done again: with the call it hands the card back.`],"finishSteps"),b=d(({route:e,key:r})=>`\`${m(e)} builder done ${r} --text "<what you checked>"\``,"finishCommand");function C({route:e,key:r,ref:c,title:o,notes:i,worktree:s,base:n,rules:t=[]}){const a=m(e);return[`You are the headless builder for card ${c} (${r}, "${o}") on the kickflip board /${e}. The board launched you; nobody watches this session, so never wait on a person. Work only in ${s}. Never restart the hub or a board server, and never push.`,"",`1. Run \`${a} builder start ${r} --kind build\`. If it refuses, stop.`,`2. Report progress with \`${a} builder progress ${r} --phase reading|building|testing|checking|landing "<plain, 80 characters or fewer>"\`, at least every 10 minutes.`,`3. To ask the orchestrator something, run \`${a} builder ask ${r} "<question>"\` and carry on; read its replies with \`${a} builder asks ${r}\`. Never block on a reply.`,`4. Record each judgment call the notes leave open with \`${a} builder call ${r} "<call>"\` instead of guessing silently.`,"",i?`Spec: the card's notes, ${i}, all sections, where later sections override earlier ones.`:"The card has no notes file: build from its title, and record what you had to decide as calls.","",...W(t),"Write tests first, and run the full suite once, in the foreground, at the end. Where the repo keeps ADRs, write one named with a timestamp: docs/adr/YYYY-MM-DD-HHMM-<slug>.md.",`Commit with \`git -c core.commentChar=';' commit -F <file>\`, the subject "${c}: <what the card now does>".`,"",`To finish, run ${b({route:e,key:r})}.`,"- If you recorded a judgment call, it hands the card back for the orchestrator to verify your calls; stop there: the orchestrator's verification finishes the card.",...f({route:e,key:r,worktree:s,base:n}),"","Never push."].join(`
2
- `)}d(C,"builderBrief");function H({route:e,key:r,ref:c,title:o,worktree:i,base:s,answers:n=[]}){const t=m(e);return[`You are the headless lander for card ${c} (${r}, "${o}") on the kickflip board /${e}. Its build is done and the orchestrator verified its judgment calls; it waits only to land. Nobody watches this session, so never wait on a person. Work only in ${i}, never restart the hub or a board server, and never push.`,"",`1. Run \`${t} builder start ${r} --kind build --label Lander\`. If it refuses, stop.`,`2. Report progress with \`${t} builder progress ${r} --phase landing "<plain, 80 characters or fewer>"\`.`,...n.length?["3. The owner answered the builder's deferred asks. Where an answer changes the build, change it, with its tests, and commit:",...n.map(a=>`- ${a.ask} "${a.question}": ${a.answer}`)]:[],"",`To finish, run ${b({route:e,key:r})}.`,...f({route:e,key:r,worktree:i,base:s}),"","Never push."].join(`
3
- `)}d(H,"landerBrief");function L({bin:e=R(),cwd:r,prompt:c,model:o,log:i}){u.mkdirSync(h.dirname(i),{recursive:!0});const s=u.openSync(i,"a"),n=Object.fromEntries(Object.entries(process.env).filter(([t])=>!k(t)));return h.isAbsolute(e)&&(n.PATH=`${h.dirname(e)}:${n.PATH||""}`),new Promise((t,a)=>{const l=w(e,["-p",c,"--permission-mode","auto","--model",o],{cwd:r,env:n,detached:!0,stdio:["ignore",s,s]});l.once("spawn",()=>{u.closeSync(s),l.unref(),t({pid:l.pid,log:i})}),l.once("error",p=>{u.closeSync(s),a(p)})})}d(L,"launchHeadless");const N=d(e=>{try{return process.kill(e,0),!0}catch(r){return r.code==="EPERM"}},"pidAlive");async function Q(e,r,{launch:c,handBack:o,alive:i=N}){const s={};for(const[n,t]of Object.entries(e))if(r.has(n)){if(t.pid!==null&&i(t.pid)){s[n]=t;continue}if(t.pid!==null&&t.relaunched){await o(t.model?{key:n,model:t.model}:{key:n});continue}s[n]={model:t.model,...await c({key:n,model:t.model}),relaunched:t.pid!==null}}return s}d(Q,"reviveBuilders");const P=d((e,r)=>h.join(g.homedir(),".claude","kickflip","logs",`${r}-${e}-${new Date().toISOString().replace(/[:.]/g,"-")}.log`),"logFile");async function V(e,{key:r,model:c},{lander:o=!1}={}){const i=await B(e,r),s=y(e)?.route??h.basename(e),n=[...x(),...await S(e)].map(p=>p.text),t={...i,route:s,base:j(e).base,rules:n},a=c||"opus",l=await L({cwd:i.worktree,prompt:o?H(t):C(t),model:a,log:P(r.split("/")[1],o?"lander":"builder")});return await T(e,r,{text:`${o?"Lander":"Builder"} launched headless on ${A({model:a})} in ${i.worktree}.`,log:h.basename(l.log)}),l}d(V,"launchBuilder");export{I as BOARD_CLI,C as builderBrief,H as landerBrief,V as launchBuilder,L as launchHeadless,Q as reviveBuilders};
1
+ var P=Object.defineProperty;var d=(t,e)=>P(t,"name",{value:e,configurable:!0});import S from"node:os";import y from"node:path";import{fileURLToPath as A}from"node:url";import{launchAgent as B}from"./agents.mjs";import{processStarts as L}from"../hub/live.mjs";import{boardAt as O}from"../hub/boards.mjs";import{readHubRules as D}from"../hub/rules.mjs";import{builderRepo as M}from"./worktree.mjs";import{appendThread as Y,briefFacts as F,readBoardRules as _,recordSession as E}from"./store.mjs";import{modelName as U,plans as H}from"./domain.mjs";const G=A(new URL("./bin/board.mjs",import.meta.url)),b=d(t=>`node ${G} --site ${t}`,"cli"),I=d(t=>`git -c core.commentChar=';' rebase ${t}`,"rebase"),T=d((t,e="Rules from the hub and this board:")=>t.length?[e,...t.map(n=>`- ${n}`),""]:[],"ruleLines"),q=d(t=>t.length?["Related cards: read their notes for context; don't build their scope.",...t.map(e=>`- ${e.ref} "${e.title}" (${e.relation}, ${e.status}): ${e.notes||"no notes"}`),""]:[],"linkLines"),j=d(({route:t,key:e,worktree:n,base:h})=>[`- With no unverified calls, done finishes the card itself: it rebases onto ${h}, reruns the full test suite, fast-forwards ${h} 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 \`${I(h)}\` 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"),R=d(({route:t,key:e})=>`\`${b(t)} builder done ${e} --text "<what you checked>"\``,"finishCommand");function K({route:t,key:e,ref:n,title:h,notes:i,worktree:s,base:a,rules:o=[],links:r=[]}){const l=b(t);return[`You are the headless builder for card ${n} (${e}, "${h}") on the kickflip board /${t}. The board launched you; nobody watches this session, so never wait on a person. Work only in ${s}. Never restart the hub or a board server, and never push.`,"",`1. Run \`${l} builder start ${e} --kind build\`. If it refuses, stop.`,`2. Report progress with \`${l} builder progress ${e} --phase reading|building|testing|checking|landing "<plain, 80 characters or fewer>"\`, at least every 10 minutes. The board adds the phase word; don't start the text with it. The phases:`," - reading: the notes and the code, until the first test is written."," - building: the tests and the code, the ADR and docs, and the commit."," - testing: the full suite run before you finish."," - checking: your own review of the diff before you hand back; the Orchestrator's review of your calls continues on the same 4th bar."," - landing: set by the finish step itself; you never report it.",`3. To ask the orchestrator something, run \`${l} builder ask ${e} "<question>"\` and carry on; read its replies with \`${l} builder asks ${e}\`. Never block on a reply.`,`4. Record each judgment call the notes leave open with \`${l} builder call ${e} "<call>"\` instead of guessing silently.`,"",i?`Spec: the card's notes, ${i}, all sections, where later sections override earlier ones.`:"The card has no notes file: build from its title, and record what you had to decide as calls.","",...q(r),...T(o),"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 ${R({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:s,base:a}),"","Never push."].join(`
2
+ `)}d(K,"builderBrief");function X({route:t,key:e,ref:n,title:h,worktree:i,base:s,answers:a=[]}){const o=b(t);return[`You are the headless lander for card ${n} (${e}, "${h}") 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 ${i}, never restart the hub or a board server, and never push.`,"",`1. Run \`${o} builder start ${e} --kind build --label Lander --phase landing --progress "taking up the verified build"\`. If it refuses, stop.`,`2. Report progress with \`${o} builder progress ${e} --phase landing "<plain, 80 characters or fewer>"\`. The board adds the phase word; don't start the text with it.`,...a.length?["3. The owner answered the builder's deferred asks. Where an answer changes the build, change it, with its tests, and commit:",...a.map(r=>`- ${r.ask} "${r.question}": ${r.answer}`)]:[],"",`To finish, run ${R({route:t,key:e})}.`,...j({route:t,key:e,worktree:i,base:s}),"","Never push."].join(`
3
+ `)}d(X,"landerBrief");function z({route:t,key:e,ref:n,title:h,worktree:i,base:s,pr:a}){const o=b(t);return[`You are the headless rebase run for card ${n} (${e}, "${h}") on the kickflip board /${t}. Its PR #${a} conflicts with ${s}; rebase its branch onto ${s} so the PR is clean again. 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 --label Rebase --phase building --progress "Rebase run started \xB7 fetching ${s}"\`. If it refuses, stop.`,`2. Run \`git fetch origin\`, then \`${I(`origin/${s}`)}\`. Resolve each conflict keeping both sides' intent, \`git add\` it and continue with \`git -c core.commentChar=';' rebase --continue\`. Report each step with \`${o} builder progress ${e} --phase building "<line>"\`, the board adds the phase word, in these words: "Rebasing onto ${s} \xB7 resolving <n> files", then "Rebasing onto ${s} \xB7 suite running".`,"3. Run the full test suite once, in the foreground. Fix what the rebase broke, and commit each fix with `git -c core.commentChar=';' commit -F <file>`. A push with commits of its own is no longer rebase-only, so a merge after an approval won't count it as trivial.",`4. If a conflict or a failing test is past what you can resolve with confidence, run \`git -c core.commentChar=';' rebase --abort\` if a rebase is under way, record it with \`${o} builder call ${e} "Rebase failed in <the files or tests> \xB7 <why>"\`, and run the finish step: with the call it hands the card to the orchestrator.`,"",`To finish, run ${R({route:t,key:e})}, the text saying what you resolved. It reruns the suite and pushes the branch to PR #${a} with --force-with-lease; the PR stays as it is.`,"- If it refuses because someone pushed to the branch since your fetch, fetch and rebase again (step 2), then run done again.","- On failing tests, fix them, commit, and run done again.","","Never push."].join(`
4
+ `)}d(z,"rebaseBrief");function J({route:t,key:e,ref:n,title:h,notes:i,kind:s,repo:a,rules:o=[],links:r=[],text:l,files:p=[],question:f,note:m,choice:$}){const c=b(t),u=s==="grill";return[`You are the headless ${u?"grill":"prototype"} planner for card ${n} (${e}, "${h}") 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 ${a}; write no output code there, never restart the hub or a board server, and never push.`,"",`1. Run \`${c} builder start ${e} --kind ${s}\`. If it refuses, stop.`,`2. Gather the context yourself: ${i?`the card's notes, ${i}, 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 \`${c} builder progress ${e} "<plain, 80 characters or fewer>"\` at least every 10 minutes.`,u?`4. Find every decision the card leaves open: read the code it touches and answer what the code answers yourself. Post each decision left as a question for the owner with \`${c} question add --file <json>\` on topic item:${e}, one per decision, your recommendation first. Invoke the grilling skill with the Skill tool and run it as one round: its questions go to the board with question add, never into the chat, and you never wait for an answer; after the round, finish. (With no Skill tool, follow this step as written.) End every grill with one more question, "Prototype round first?", whose options are yes and no, with "prototype_gate": true in its \`question add --file\` entry: the board moves the card to prototyping on yes, or to ready on no once the owner has answered the rest, so nothing else moves it. Recommend yes when the card changes the hub's or a board's UI, no otherwise. Ask it even when it is your only question.`:`4. Record the prototype with \`${c} prototype add <topic> --card ${e} --take <K>:<label> \u2026\`, write one take per option as a page built from the board's real components fed fake data, and post one question whose url opens it with \`${c} question add --file <json>\` on topic item:${e}, never asking in the chat. Invoke the prototype skill with the Skill tool for this step. (With no Skill tool, follow this step as written.)`,...l?[` The owner asked for this ${u?"grill":"prototype"}, writing: "${l}"`]:[],...p.length?[` The owner attached: ${p.map(g=>g.path).join(", ")}.`]:[],...f?[` The owner asked for prototypes of question ${f}${m?`, noting "${m}"`:""}${$?`, leaning to option ${$}`:""}: draw that question's next round, its question naming round_of ${f}.`]:[],`5. Record what you learned in the card's notes with \`${c} item set ${e} --notes-file <file.md>\`, naming each answered question's id.`,...u?[`6. Where the owner asked for prototypes, or a rule here wants a UI prototype first, end the card \`${c} item set ${e} --state prototyping\` instead and skip the prototype question. Otherwise the prototype question is what moves the card on: never set it ready yourself, and never leave a Next that asks the owner to mark it ready.`]:[],"",...q(r),...T(o,"Rules from the hub and this board, written for the orchestrator: where one tells you to push, land, restart the hub or a board server, or do the orchestrator's work, it isn't yours; you never push or restart:"),`To finish, run \`${c} builder done ${e} --text "<what you asked>"\`.`,"","Never push."].join(`
5
+ `)}d(J,"plannerBrief");const Q=6e4;function ce(t,{starts:e=L,kill:n=d((i,s)=>process.kill(-i,s),"kill"),killAfter:h=5e3}={}){const i=t.filter(o=>o.pid&&o.at).map(o=>o.pid),s=i.length&&e(i)||new Map,a=t.filter(o=>{const r=s.get(o.pid);return r!==void 0&&r<=Date.parse(o.at)&&r>=Date.parse(o.at)-Q});for(const{pid:o}of a){try{n(o,"SIGTERM")}catch{continue}setTimeout(()=>{try{n(o,"SIGKILL")}catch{}},h).unref()}return a.length}d(ce,"stopAgents");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:h,alive:i=V,held:s=d(()=>!1,"held")}){const a={};for(const[o,r]of Object.entries(t)){if(!e.has(o))continue;if(r.pid!==null&&i(r.pid)||s(r.kind??"claude")||r.handedBack){a[o]=r;continue}const l={key:o,...r.kind?{kind:r.kind}:{},...r.model?{model:r.model}:{}};if(r.pid!==null&&r.relaunched){await h(l),a[o]={...r,handedBack:!0};continue}const{key:p,...f}=l;a[o]={...f,...await n(l),relaunched:r.pid!==null}}return a}d(ue,"reviveBuilders");const Z=d((t,e)=>y.join(S.homedir(),".claude","kickflip","logs",`${e}-${t}-${new Date().toISOString().replace(/[:.]/g,"-")}.log`),"logFile");async function pe(t,{key:e,kind:n="claude",model:h,text:i,files:s,question:a,note:o,choice:r},{lander:l=!1}={}){const p=await F(t,e),f=O(t)?.route??y.basename(t),m=[...D(),...await _(t)].map(C=>C.text),{base:$,repo:c}=M(t),u=H({kind:n}),g=!u&&!l&&p.rebasePR!==null,v={...p,route:f,base:$,repo:c,rules:m,kind:n,text:i,files:s,question:a,note:o,choice:r,pr:p.rebasePR},k=u?"Planner":l?"Lander":g?"Rebase run":"Builder",x=u?c:p.worktree,N=u?J(v):l?X(v):g?z(v):K(v),w=await B({kind:u?"claude":n,model:h,cwd:x,site:t,prompt:N,log:Z(e.split("/")[1],k.split(" ")[0].toLowerCase())});w.sessionId&&await E(t,e,w.sessionId,w.pid);const W=w.sessionId?`, session ${w.sessionId}`:"";return await Y(t,e,{text:`${k} launched on ${U({kind:n,model:h})} in ${x}${W}.`,log:y.basename(w.log)}),w}d(pe,"launchBuilder");export{G as BOARD_CLI,K as builderBrief,X as landerBrief,pe as launchBuilder,V as pidAlive,J as plannerBrief,z as rebaseBrief,ue as reviveBuilders,ce as stopAgents};
@@ -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 l=Object.defineProperty;var r=(e,s)=>l(e,"name",{value:s,configurable:!0});import f from"node:os";import{execFile as u}from"node:child_process";import{promisify as m}from"node:util";const g=m(u),p=r(e=>String(e||"").trim().replace(/^::ffff:/,""),"bare"),d=r(()=>Object.values(f.networkInterfaces()).flat().map(e=>e.address),"interfaceAddresses");async function i(){try{const{stdout:e}=await g("tailscale",["status","--json"],{timeout:5e3}),{Self:s,User:n}=JSON.parse(e);return{login:n?.[s.UserID]?.LoginName,addresses:s.TailscaleIPs||[]}}catch{return null}}r(i,"tailnetNode");const c=r(e=>{const s=e.headers["x-forwarded-for"];return p(s?String(s).split(",").at(-1):e.socket.remoteAddress)},"clientOf");async function S(e,s={addresses:d,node:i}){const n=e.headers["x-forwarded-for"],t=c(e);if(s.addresses().includes(t))return!0;const a=e.headers["tailscale-user-login"];if(!n||!a)return!1;const o=await s.node();return!!o&&o.login===a&&o.addresses.includes(t)}r(S,"fromHost");async function h(e,s={addresses:d,node:i}){if(s.addresses().includes(c(e)))return!0;const n=e.headers["tailscale-user-login"];if(!n)return!1;const t=await s.node();return!!t&&t.login===n}r(h,"fromOwner");export{S as fromHost,h as fromOwner};
1
+ var p=Object.defineProperty;var s=(e,n)=>p(e,"name",{value:n,configurable:!0});import w from"node:os";import{execFile as y}from"node:child_process";import{promisify as N}from"node:util";import{tailscaleBin as h}from"../shared/tailscale.mjs";const x=N(y),S=s(e=>String(e||"").trim().replace(/^::ffff:/,""),"bare"),f=s(()=>Object.values(w.networkInterfaces()).flat().map(e=>e.address),"interfaceAddresses");async function m(){const e=h();if(!e)return null;try{const{stdout:n}=await x(e,["status","--json"],{timeout:5e3}),{Self:o,User:r}=JSON.parse(n),t=r?.[o.UserID];return{login:t?.LoginName,name:t?.DisplayName||null,dnsName:String(o.DNSName||"").replace(/\.$/,"")||null,addresses:o.TailscaleIPs||[]}}catch{return null}}s(m,"tailnetNode");const g=s(e=>{const n=e.headers["x-forwarded-for"];return S(n?String(n).split(",").at(-1):e.socket.remoteAddress)},"clientOf");async function j(e,n={addresses:f,node:m}){const o=e.headers["x-forwarded-for"],r=g(e);if(n.addresses().includes(r))return!0;const t=e.headers["tailscale-user-login"];if(!o||!t)return!1;const a=await n.node();return!!a&&a.login===t&&a.addresses.includes(r)}s(j,"fromHost");function D(e={addresses:f,node:m},{now:n=Date.now,retryMs:o=5e3}={}){let r=null,t=null,a=-1/0;const l=s(()=>r||n()-a<o?r:(t||=e.node().then(i=>(t=null,i?.login?r=i:a=n(),r)),t),"ownerNode"),d=s(async i=>{if(e.addresses().includes(g(i)))return"owner";const c=i.headers["tailscale-user-login"];if(!c)return"other";const u=await l();return u?u.login===c?"owner":"other":"unread"},"gate");return d.node=async()=>l(),d}s(D,"ownerGate");const B=D();export{j as fromHost,B as hubGate,D as ownerGate};
@@ -1,2 +1,2 @@
1
- var S=Object.defineProperty;var a=(s,t)=>S(s,"name",{value:t,configurable:!0});import c from"node:fs/promises";import p from"node:path";import{BoardError as d}from"./store.mjs";const u=[{id:"pushMain",name:"Push main after a land",allows:"Pushes main to origin as each card lands, and lets the session push it.",rule:"Bash(git push origin main)"},{id:"pushTags",name:"Push release tags",allows:"Lets the session push a v* release tag.",rule:"Bash(git push origin v*)"},{id:"draftPr",name:"Open draft PRs",allows:"Lets the session open a PR as a draft.",rule:"Bash(gh pr create --draft *)"}];function f(s){if(typeof s!="string"||!p.isAbsolute(s))throw new d("This board has no repo folder recorded, so it has no settings.local.json to write. Set its repo first.");return p.join(s,".claude","settings.local.json")}a(f,"settingsFile");const m=a(s=>p.join(s,"data","permission-refusals.json"),"refusalsFile");async function y(s,t,e){let n;try{n=await c.readFile(s,"utf8")}catch(i){if(i.code==="ENOENT")return t;throw i}try{const i=JSON.parse(n);if(i&&typeof i=="object"&&!Array.isArray(i))return i}catch{}throw new d(`${e} isn\u2019t a JSON object, so the hub leaves it as it is. Fix it by hand first.`)}a(y,"readJSON");async function g(s,t){await c.mkdir(p.dirname(s),{recursive:!0});const e=`${s}.${process.pid}.${Date.now()}.tmp`;await c.writeFile(e,JSON.stringify(t,null,2)+`
2
- `),await c.rename(e,s)}a(g,"writeJSON");const j=a(s=>Array.isArray(s.permissions?.allow)?s.permissions.allow:[],"allowOf");async function x(s){const t=j(await y(f(s),{},f(s)));return Object.fromEntries(u.map(e=>[e.id,t.includes(e.rule)]))}a(x,"readPermissions");async function A(s,t,e,n){const i=u.find(l=>l.id===e);if(!i||typeof n!="boolean")throw new d(`Send {id, on} with id one of ${u.map(l=>l.id).join(", ")} and on true or false.`);const r=f(s),o=await y(r,{},r);if(o.permissions!==void 0&&(typeof o.permissions!="object"||Array.isArray(o.permissions)||o.permissions===null))throw new d(`${r}\u2019s permissions isn\u2019t an object, so the hub leaves it as it is. Fix it by hand first.`);const w=j(o);w.includes(i.rule)!==n&&(o.permissions={...o.permissions,allow:n?[...w,i.rule]:w.filter(l=>l!==i.rule)},await g(r,o));const h=await b(t);return h[e]&&(delete h[e],await g(m(t),h)),x(s)}a(A,"setPermission");const b=a(s=>y(m(s),{},m(s)),"readRefusals");function O(s){return typeof s!="string"?null:u.find(t=>{const e=/^Bash\((.*)\)$/.exec(t.rule)[1];return new RegExp(`^${e.split("*").map(n=>n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join(".*")}$`).test(s.trim())})||null}a(O,"permissionFor");async function F(s,t,e,n=new Date){if(e?.tool_name!=="Bash")return null;const i=O(e.tool_input?.command);if(!i||!(await x(s))[i.id])return null;const r=await b(t);return r[i.id]={command:e.tool_input.command,how:e.hook_event_name==="PermissionDenied"?"denied":"asked",at:n.toISOString()},await g(m(t),r),i.id}a(F,"recordRefusal");async function B(s,t,e){const[n,i]=await Promise.all([x(s),b(t)]);return{owner:e,file:f(s),permissions:u.map(r=>({...r,on:n[r.id],refused:n[r.id]&&i[r.id]||null}))}}a(B,"permissionsView");export{u as PERMISSIONS,O as permissionFor,B as permissionsView,x as readPermissions,b as readRefusals,F as recordRefusal,A as setPermission,f as settingsFile};
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};
@@ -1,102 +1,19 @@
1
- // New tasks on the board view: the + sheet that sends one as an ask event, and bump, edit and delete for a New card or a
1
+ // New tasks on the board view: bump, edit and delete for a New card or a
2
2
  // pending thread message (docs/adr/2026-10-29-1639-a-pending-ask-can-be-deleted-or-bumped.md,
3
3
  // docs/adr/2026-10-29-1639-the-plus-makes-a-new-card.md). Needs app-core.js and app-threads.js.
4
4
  BoardView.part((B) => {
5
- const { $, store, state, toast, quieted, sendDraft } = B;
6
- const { attachButton, waitOn } = BoardUI;
5
+ const { $, state, toast } = B;
6
+ const { waitOn, keyedSend } = BoardUI;
7
7
 
8
- // The ask box is a bottom sheet: a <dialog> opened from the + button. Closing keeps the draft.
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
+ // ⌘↵ and ⌥↵ press a thread reply's or an edit's send buttons from its box (ui.js keyedSend); the ask's own are new-task.js's.
17
9
  B.on(B.root, 'keydown', (ev) => {
18
- if (ev.key !== 'Enter' || !(ev.metaKey || ev.ctrlKey) || ev.isComposing) return;
19
- const form = ev.target instanceof HTMLTextAreaElement ? ev.target.closest('form[data-composer], form.edit-form') : null;
20
- if (!form) return;
21
- ev.preventDefault();
22
- form.requestSubmit();
23
- });
24
- $('#askText').value = store.get('draft:ask', '');
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;
10
+ const form = ev.target.tagName === 'TEXTAREA' ? ev.target.closest('form[data-reply], form.edit-form') : null;
11
+ const button = form && keyedSend(ev, form);
12
+ if (!button) return;
59
13
  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
- // Discuss or Quick build, above Send: Discuss, the default, has the session grill or prototype the task at once; Quick
66
- // build sends the ask with quick: true, and the session marks the card ready. Each send goes back to Discuss
67
- // (docs/adr/2026-11-04-0131-every-new-task-is-grilled-or-prototyped-at-once.md).
68
- const CHOICE_HELP = {
69
- discuss: 'Discuss: Claude grills or prototypes it first, then waits for you.',
70
- quick: 'Quick build: for changes you’re sure about; Claude marks it ready and builds.',
71
- };
72
- let quick = false;
73
- function choose(isQuick) {
74
- quick = isQuick;
75
- $('#askDiscuss').setAttribute('aria-pressed', String(!quick));
76
- $('#askQuick').setAttribute('aria-pressed', String(quick));
77
- $('#askChoiceHelp').textContent = CHOICE_HELP[quick ? 'quick' : 'discuss'];
78
- }
79
- choose(false);
80
- B.on(B.root, 'click', (ev) => {
81
- const b = ev.target.closest('#askDiscuss, #askQuick');
82
- if (b) choose(b.id === 'askQuick');
83
- });
84
- B.on($('#askForm'), 'submit', async (ev) => {
85
- ev.preventDefault();
86
- $('#askText').blur();
87
- const text = $('#askText').value.trim();
88
- const files = BoardAttach.files('ask', B.site.root);
89
- if ((!text && !files.length) || BoardAttach.busy('ask', B.site.root)) return;
90
- $('#askStatus').textContent = '';
91
- try {
92
- await waitOn($('#askForm button[type="submit"]'), sendDraft('draft:ask', $('#askText'), () => $('#askText'), () => B.send('ask', { text, ...(files.length ? { files } : {}), ...(quick ? { quick: true } : {}) })));
93
- BoardAttach.clear('ask', B.site.root);
94
- choose(false);
95
- $('#askStatus').textContent = '';
96
- closeSheet();
97
- toast('Sent');
98
- } catch { $('#askStatus').textContent = 'Could not send'; }
14
+ form.requestSubmit(button);
99
15
  });
16
+ BoardNewTask.wire(B);
100
17
 
101
18
  // A bump moves the New card to the top once the server has recorded it, button (its icon, or the prototype button
102
19
  // that bumps it) waiting meanwhile (docs/adr/2026-11-03-1850-a-tap-waits-for-the-server-to-record-it.md).