@lovek4d/kickflip 0.1.0 → 0.4.0

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