@lovek4d/kickflip 0.7.4 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONTEXT.md CHANGED
@@ -299,7 +299,7 @@ _Avoid_: backlog item, todo
299
299
  _Avoid_: history, old cards
300
300
 
301
301
  **Lane**:
302
- Where a card sits (In Review, In Progress, Todo, Done), set by who acts next ([the lane follows who acts next, and a card waiting on the owner says Action needed](docs/adr/2026-10-29-1639-the-lane-follows-who-acts-next.md)). In Progress means a builder runs on the card: a running build agent, a **checking** item, or a started card between runs; a ready PR whose CI is red, or a red draft on a board that **skips the draft**, waits in In Review reading "Fix the failing CI" until a fix builder starts ([In Progress means a builder runs](docs/adr/2026-11-09-1804-in-progress-means-a-builder-runs.md)). A work card stores its **lane** (todo, progress, review, done, with `lane_at`), which one writer moves with a "Moved to <lane>: <why>" thread line; a card has no status, and held, blocked, merged and dropped are facts beside the lane ([a card stores its lane, and facts replace statuses](docs/adr/2026-11-07-1400-a-card-stores-its-lane-and-facts-replace-statuses.md)). A work card that has left Todo never drops back to it: held, blocked or **bumped out**, its build takes a **card pause** In Progress ([a card pause stops its builder at its next stopping point](docs/adr/2026-11-09-1300-a-card-pause-stops-its-builder-at-its-next-stopping-point.md), [a card moves to In Progress only once building starts](docs/adr/2026-11-03-0202-a-card-moves-to-in-progress-only-once-building-starts.md)). A running grill or prototype agent shows its status line but leaves the card where it is. In Review means finished work waits on a person: the reviewers, or the owner's review of a draft ("Review draft PR #n", whatever its CI), Merge, manual steps or the Done of an ask (see **Action needed**), or of a reopened card. An owner message or a new question on a Done card, merged or not, reopens it into In Review: "Your reply waits on the orchestrator" until Claude answers, then the owner's Done, its next step "Tap Done once it is settled" ([a message or question on a Done card reopens it until the owner's Done](docs/adr/2026-11-02-1952-a-message-or-question-on-a-done-card-reopens-it.md), [a merged card reopens on a message, and its PR badge shows the PR's state](docs/adr/2026-11-03-1843-a-merged-card-reopens-and-its-pr-badge-shows-the-prs-state.md), [a card waiting only to be closed has no Action needed](docs/adr/2026-11-03-0137-a-card-waiting-only-to-be-closed-has-no-action-needed.md)). A reopened card goes to In Progress only once a builder starts on it ([In Progress means a builder runs](docs/adr/2026-11-09-1804-in-progress-means-a-builder-runs.md)). A card waiting only on those moves there from In Progress; one Claude still works on stays in In Progress. A card with several PRs, in any repos, sits where its least-ready PR puts it, shows a PR badge for each, and is Done once every PR has merged or closed with at least one merged ([a card holds several PRs across repos](docs/adr/2026-11-03-1904-a-card-holds-several-prs-across-repos.md)). Todo holds work that hasn't started building: the New cards, which wait on Claude ([the + makes a New card, which Claude files, folds or answers](docs/adr/2026-10-29-1639-the-plus-makes-a-new-card.md)), and every work card with no build yet, where its **Implement** shows. Discussion keeps a card there: open questions or a prototype pick, with Action needed, answered or resolved questions, grill and prototype agents, a working mark on a message in its thread and a prototype recorded for it ([a card moves to In Progress only once building starts](docs/adr/2026-11-03-0202-a-card-moves-to-in-progress-only-once-building-starts.md)). A started card that gets a question keeps its lane ([an unstarted card waits on its questions in Todo, and its one action button follows what it waits on](docs/adr/2026-10-30-1309-an-unstarted-card-waits-on-its-questions-in-todo.md)). Each lane has one fixed order, with no sort menu ([each lane has one fixed order](docs/adr/2026-11-03-0223-each-lane-has-one-fixed-order.md)): every lane but Done lists its bumped cards first, the earliest bump on top ([a bump marks the priority through every lane](docs/adr/2026-11-06-1700-a-bump-marks-the-priority-through-every-lane.md)); Todo then lists the rest oldest made first, so a new card joins the bottom ([new Todo cards join the bottom, so old ones leave first](docs/adr/2026-11-05-2345-new-todo-cards-join-the-bottom.md)); In Progress, after its bumped cards, by when each came into the lane, its reopening or its latest build agent's start, the newest arrival on top; In Review, after its bumped cards, by when the PR was first sent to a reviewer, or a reopened card's reopening, the newest on top; Done by when each card closed, or for an answered ask when Claude last answered, the most recent on top. Done shows 10 cards, then a Show more, the thread's, that adds 10 and says how many are left; a card the owner has open stays shown ([answered asks fold into the one Done list, the kind shown in the ticket slot](docs/adr/2026-11-09-1430-answered-asks-fold-into-the-one-done-list.md)). Every card, a New card included, ends with one muted line holding the time its lane sorts by: the time only today, the day too before today ("10/31, 2:49 PM"), and the year too past 365 days. A card with no such time has no time on that line.
302
+ Where a card sits (In Review, In Progress, Todo, Done), set by who acts next ([the lane follows who acts next, and a card waiting on the owner says Action needed](docs/adr/2026-10-29-1639-the-lane-follows-who-acts-next.md)). In Progress means a builder runs on the card: a running build agent, a **checking** item, or a started card between runs; a ready PR whose CI is red, or a red draft on a board that **skips the draft**, waits in In Review reading "Fix the failing CI" until a fix builder starts ([In Progress means a builder runs](docs/adr/2026-11-09-1804-in-progress-means-a-builder-runs.md)). A work card stores its **lane** (todo, progress, review, done, with `lane_at`), which one writer moves with a "Moved to <lane>: <why>" thread line; a card has no status, and held, blocked, merged and dropped are facts beside the lane ([a card stores its lane, and facts replace statuses](docs/adr/2026-11-07-1400-a-card-stores-its-lane-and-facts-replace-statuses.md)). A work card that has left Todo never drops back to it: held, blocked or **bumped out**, its build takes a **card pause** In Progress ([a card pause stops its builder at its next stopping point](docs/adr/2026-11-09-1300-a-card-pause-stops-its-builder-at-its-next-stopping-point.md), [a card moves to In Progress only once building starts](docs/adr/2026-11-03-0202-a-card-moves-to-in-progress-only-once-building-starts.md)). A running grill or prototype agent shows its status line but leaves the card where it is. In Review means finished work waits on a person: the reviewers, or the owner's review of a draft ("Review draft PR #n", whatever its CI), Merge, manual steps or the Done of an ask (see **Action needed**), or of a reopened card. An owner message or a new question on a Done card, merged or not, reopens it into In Review: "Your reply waits on the orchestrator" until Claude answers, then the owner's Done, its next step "Tap Done" ([a message or question on a Done card reopens it until the owner's Done](docs/adr/2026-11-02-1952-a-message-or-question-on-a-done-card-reopens-it.md), [a merged card reopens on a message, and its PR badge shows the PR's state](docs/adr/2026-11-03-1843-a-merged-card-reopens-and-its-pr-badge-shows-the-prs-state.md), [a card waiting only to be closed has no Action needed](docs/adr/2026-11-03-0137-a-card-waiting-only-to-be-closed-has-no-action-needed.md)). A reopened card goes to In Progress only once a builder starts on it ([In Progress means a builder runs](docs/adr/2026-11-09-1804-in-progress-means-a-builder-runs.md)). A card waiting only on those moves there from In Progress; one Claude still works on stays in In Progress. A card with several PRs, in any repos, sits where its least-ready PR puts it, shows a PR badge for each, and is Done once every PR has merged or closed with at least one merged ([a card holds several PRs across repos](docs/adr/2026-11-03-1904-a-card-holds-several-prs-across-repos.md)). Todo holds work that hasn't started building: the New cards, which wait on Claude ([the + makes a New card, which Claude files, folds or answers](docs/adr/2026-10-29-1639-the-plus-makes-a-new-card.md)), and every work card with no build yet, where its **Implement** shows. Discussion keeps a card there: open questions or a prototype pick, with Action needed, answered or resolved questions, grill and prototype agents, a working mark on a message in its thread and a prototype recorded for it ([a card moves to In Progress only once building starts](docs/adr/2026-11-03-0202-a-card-moves-to-in-progress-only-once-building-starts.md)). A started card that gets a question keeps its lane ([an unstarted card waits on its questions in Todo, and its one action button follows what it waits on](docs/adr/2026-10-30-1309-an-unstarted-card-waits-on-its-questions-in-todo.md)). Each lane has one fixed order, with no sort menu ([each lane has one fixed order](docs/adr/2026-11-03-0223-each-lane-has-one-fixed-order.md)): every lane but Done lists its bumped cards first, the earliest bump on top ([a bump marks the priority through every lane](docs/adr/2026-11-06-1700-a-bump-marks-the-priority-through-every-lane.md)); Todo then lists the rest oldest made first, so a new card joins the bottom ([new Todo cards join the bottom, so old ones leave first](docs/adr/2026-11-05-2345-new-todo-cards-join-the-bottom.md)); In Progress, after its bumped cards, by when each came into the lane, its reopening or its latest build agent's start, the newest arrival on top; In Review, after its bumped cards, by when the PR was first sent to a reviewer, or a reopened card's reopening, the newest on top; Done by when each card closed, or for an answered ask when Claude last answered, the most recent on top. Done shows 10 cards, then a Show more, the thread's, that adds 10 and says how many are left; a card the owner has open stays shown ([answered asks fold into the one Done list, the kind shown in the ticket slot](docs/adr/2026-11-09-1430-answered-asks-fold-into-the-one-done-list.md)). Every card, a New card included, ends with one muted line holding the time its lane sorts by: the time only today, the day too before today ("10/31, 2:49 PM"), and the year too past 365 days. A card with no such time has no time on that line.
303
303
  _Avoid_: column, status (status is the item's own field)
304
304
 
305
305
  **Checking**:
@@ -375,7 +375,7 @@ The open card's link to its PR, on every card with an open PR, last on the detai
375
375
  _Avoid_: Review #<n> on GitHub, PR link
376
376
 
377
377
  **Install**:
378
- The app card's one badge that opens its build on the owner's phone, or the badge on main's slim bar above the lanes. A ready card reads "Install ↗", or "Install (stale) ↗" when its build's sha differs from the current branch head. While a build runs it reads "Serving…" with an inert spinner; a failed build reads "Serve failed ↗" and opens its thread. The open card's badge sits between Notes and Review on GitHub. The workflow button starts a Serve, or Retry after failure, while Auto serve is off and the card's branch has changes ([only the owner or Auto serve starts a card run](docs/adr/2026-11-02-1806-only-the-owner-or-auto-build-starts-a-card-run.md)). While main re-serves, its old server keeps serving until the new one answers: the bar names the old commit and keeps Visit to it, with a Serving… chip left of Visit, or a Failed button there when the new build failed ([a Main re-serve keeps the old server up until the new one answers](docs/adr/2026-11-09-1815-a-main-re-serve-keeps-the-old-server-up-until-the-new-one-answers.md)). Main’s red Build failed button opens a bottom sheet with the last 40 lines of `data/try/main.log` and Retry at the recorded failed tip. Retry shows readiness refusals in the sheet, reads Building… in orange while running, refreshes the log after failure and closes with a toast when ready. On a local board, a refresh icon right of main's timestamp pulls its branch from origin now, as every poll does, and turns red with the reason when a pull can't fast-forward; a tap or click always pulls again, on any device, and opens the reason when that pull fails ([a local board's Main pulls from origin on its own](docs/adr/2026-11-03-1914-a-local-boards-main-pulls-from-origin-on-its-own.md)). A Done card has no try badge ([a board's build script serves or builds each card and main](docs/adr/2026-11-01-1846-a-boards-build-script-serves-or-builds-each-card-and-main.md)).
378
+ The app card's one badge that opens its build on the owner's phone, or the badge on main's slim bar above the lanes. A ready card reads "Install ↗", or "Install (stale) ↗" when its build's sha differs from the current branch head. While a build runs it reads "Serving…" with an inert spinner; a failed build reads "Serve failed ↗" and opens its thread. The open card's badge sits between Notes and Review on GitHub. The workflow button starts a Serve, or Retry after failure, while Auto serve is off and the card's branch has changes ([only the owner or Auto serve starts a card run](docs/adr/2026-11-02-1806-only-the-owner-or-auto-build-starts-a-card-run.md)). Main's bar is two rows: the repo's owner/name with Visit right-justified, then main@sha, its age as muted text and the refresh icon, with Updating… (spinner) or Update failed right-justified, even with no old server. While main re-serves, its old server keeps serving until the new one answers: the bar names the old commit and keeps Visit to it ([a Main re-serve keeps the old server up until the new one answers](docs/adr/2026-11-09-1815-a-main-re-serve-keeps-the-old-server-up-until-the-new-one-answers.md)); a new serve that exits before answering stops the old server and serves once more ([Main serves again after the pair update](docs/adr/2026-11-10-0108-main-serves-again-after-the-pair-update.md)). Main’s red Update failed button opens a bottom sheet with the last 40 lines of `data/try/main.log` and Retry at the recorded failed tip. Retry shows readiness refusals in the sheet, reads Building… in orange while running, refreshes the log after failure and closes with a toast when ready. On a local board, a refresh icon right of main's timestamp pulls its branch from origin now, as every poll does, and turns red with the reason when a pull can't fast-forward; a tap or click always pulls again, on any device, and opens the reason when that pull fails ([a local board's Main pulls from origin on its own](docs/adr/2026-11-03-1914-a-local-boards-main-pulls-from-origin-on-its-own.md)). A Done card has no try badge ([a board's build script serves or builds each card and main](docs/adr/2026-11-01-1846-a-boards-build-script-serves-or-builds-each-card-and-main.md)).
379
379
  _Avoid_: download, TestFlight (a different channel), older build
380
380
 
381
381
  **Build script**:
package/board/domain.mjs CHANGED
@@ -1,4 +1,4 @@
1
- var Vt=Object.defineProperty;var d=(e,t)=>Vt(e,"name",{value:t,configurable:!0});import{routeRecord as R}from"../hub/routes.mjs";import Xt from"node:fs";import{ticketBadge as pt,ticketIn as Yt}from"./tickets.mjs";import{prLinks as zt,reviewLinks as Jt,reviewTime as Zt,reviewersOf as en}from"./review.mjs";import{remoteFromInput as tn}from"../hub/repos.mjs";import{reviewsOf as nn,reviewNeeds as sn,reviewActions as on}from"./auto-review.mjs";import{cardPRs as rn,hasPR as et,leastReady as an,prKeyOf as ln,prURL as dn}from"../shared/card-prs.mjs";import ft from"node:vm";import{checkWords as cn,parseCheck as un}from"../shared/owner-check.mjs";import{buildScriptOf as pn}from"./build-script.mjs";import{HUB as Qe,hubLineOf as gt}from"./hub-lines.mjs";const fn="card-merged",tt=ft.createContext({});for(const e of["ui-enums.js","ui-order.js"])ft.runInContext(Xt.readFileSync(new URL(`./public/${e}`,import.meta.url),"utf8"),tt);const{BUTTON:ce,ACTION_KIND:q,BUILDER_KIND:ue,PR_STATE:V,PHASES:kt}=tt.BoardUI,{laneOrder:gn}=tt.BoardUI,kn=["item-message","mark-ready","grill-more","prototype-request","visual-request","reask-request"],mt="question-reopen",bo=Object.freeze({todo:"Todo",progress:"In Progress",review:"In Review",done:"Done"}),Ee="move",mn=10*6e4,wn=5*6e4,wt="build-refused",Ne=Object.freeze({ask:"builder-ask",answer:"builder-answer"}),hn=864e5,pe=d(e=>e.lane==="done","closedLane"),yn=d(e=>`file:${e}`,"fileFold"),ht=7*hn,yt=d((e,t)=>({id:e,url:t?.workspace?`https://linear.app/${t.workspace}/issue/${e}`:null}),"linearRef"),bn=d(e=>String(e).replace(/^S(\d+)$/,"INT-$1"),"internalRef"),nt=d((e,t,n)=>Yt(n,t.ticket)?t.ticket:e.internal===!0?bn(t.id):t.id,"refOf"),Sn=d((e,t,n)=>pt(e,t,n)||(t?{id:String(t),url:null,source:null}:null),"ticketOf");function vn(e,t,n,s,r,a,c,l,u){return!a&&(e.lane==="done"||t?.state==="MERGED"||t?.state==="CLOSED")?"done":(n||s)&&!l?"progress":r?"review":t?.state==="OPEN"?t.checks?.state==="fail"&&(!t.isDraft||c)?"progress":"review":t||u?"progress":"todo"}d(vn,"laneOf");const He="Lander",bt=d(e=>e.kind!==ue.install&&e.label!==He,"changesCode");function xn(e,t){return e?.state==="MERGED"||t?V.merged:e?.state==="CLOSED"?V.closed:e?.state==="OPEN"?e.isDraft?V.draft:V.open:null}d(xn,"prStateOf");const St=d(e=>e.pr?.status===V.closed,"unlanded"),On=d(e=>e.some(t=>t.kind==="work")&&e.every(t=>t.lane==="done"),"finishedOf");function st(e){const t=vt(e);return t?.done&&t.handed_back?t.handed_back:null}d(st,"handedBackAt");const vt=d(e=>e.filter(t=>t?.started&&Q(t)&&t.kind!==ue.install).sort((t,n)=>String(t.started).localeCompare(String(n.started))).at(-1),"newestBuild");function En(e,t,n){return Object.fromEntries(e.map(s=>{const r=R(t?.reviews,s.github),a=r==="APPROVED"?"approved":r==="CHANGES_REQUESTED"?"changes":Object.hasOwn(n,s.name)?"sent":"unsent";return[s.name,{state:a,since:a==="sent"?R(n,s.name):null,github:s.github||null}]}))}d(En,"reviewOf");function $n(e,t,n,s,r,a,{held:c,needsApproval:l}){return n?.kind===ue.install?"builder":s?"checking":n?"builder":c&&e==="review"?"merge":r&&e==="review"?"owner":e==="review"&&t?.state==="OPEN"&&t.isDraft?a?"ci":"ready":e==="review"?xt(t,l)?"merge":"reviewers":null}d($n,"waitsOnOf");const xt=d((e,t)=>e?.reviewDecision==="APPROVED"||!t&&!["REVIEW_REQUIRED","CHANGES_REQUESTED"].includes(e?.reviewDecision),"reviewed"),De=/(^|\/)prototype-[\w-]+\.html/,An={reviewers:"Waiting for PR reviewers",merge:"Approved, merges once CI and the merge state allow",ci:"Marked ready for review once CI passes"},Ve={waiting:"Approved, Merge shows once CI and the merge state allow",ready:"Approved and green: tap Merge",merging:"Merging"},ot={auto:"Ready, merges once CI and the merge state allow",waiting:"Ready, Merge shows once CI and the merge state allow",ready:"Ready and green: tap Merge"},Bn=d(e=>`Verified: tap Merge to land it on ${e}`,"heldNext"),_n="Fix the failing CI",Rn="Your reply waits on the orchestrator",Mn="Tap Done once it is settled",In=["status","agent","ask-link","command",Ee,Ne.answer],qn=d(e=>(e.who==="claude"||e.via==="github")&&!In.includes(e.kind)&&(e.kind!==Qe||gt(e).fault),"countsUnread"),Cn=["lander-handoff","land-waiting"];function Nn({slug:e,itemFile:t,it:n,order:s,prs:r,repo:a,remote:c,base:l,builder:u,reachedPhase:m,agents:f,worktree:p,eta:w,thread:v,events:_,working:x,said:P,askedAt:C,topicItems:L,prSlack:T,boardReviewers:W,now:h,bumpedAt:b,holdAsk:i,closedAt:S,mergeAt:oe,autoMerge:ke,skipDraft:me,tickets:K}){const F=r.map(k=>({repo:k.repo,number:k.number,url:dn(k),...k.gh||k.kept||{},status:xn(k.gh,k.merged_at)})),ee=an(r.map(k=>k.gh)),I=r[ee]?.gh||null,G=F[ee]?.status??null,ze=n.lane||null,U=!!S&&String(S)>String(n.lane_at||"");U&&(n={...n,lane:"done",closed_at:S,reopened_at:void 0});const N=[n.closed_at,...r.flatMap(k=>[k.merged_at,k.gh?.mergedAt])].filter(Boolean).map(String).sort().pop()||null,we=String(N||""),he=F.length>0&&F.every(k=>k.status===V.closed),ye=(n.lane==="done"||n.reopened_at||G===V.merged)&&!he&&([...P.filter(k=>k.kind==="item-message").map(k=>k.ts),...C].map(String).filter(k=>k>we).sort().pop()||n.reopened_at)||void 0,Le=n.answered_at&&!n.phase&&!n.closed_at&&!ye?String(n.answered_at):void 0,j=ye||Le,Re=n.owner_step&&!U?String(n.owner_step):null,Je=(n.lane==="done"||G===V.merged)&&!he,H=!!j&&!!n.next&&String(n.next_at||"")>String(n.reopened_at||N||"");j&&!H&&(n={...n,next:void 0});const X=st(f),Te=j&&P.filter(k=>String(k.ts)>we&&(k.state==="waiting"||k.state==="working")).map(k=>String(k.ts)).sort()[0]||null,be=!!Te,Se=`${e}/${n.id}`,ve=en(t,W),Pe=Object.fromEntries(Object.entries(Jt(t,n,Se,L,zt(T,r))).map(([k,de])=>[k,Zt(de.ts)])),te=Object.values(Pe).filter(Boolean).sort()[0]||null,Ue=[...v.map(k=>k.ts),..._.map(k=>k.ts)].sort().pop()||null,je=[...v.filter(k=>qn({...k,who:k.who||"claude"})).map(k=>k.ts),..._.map(k=>k.reply?.at)].filter(Boolean).sort(),Y=!et(n)&&n.pushed?.branch?{branch:n.pushed.branch,at:n.pushed.at||null,host:c?.host||null}:null,z=!et(n)&&n.merge_ready?.branch?{branch:n.merge_ready.branch,at:n.merge_ready.at||null,base:l}:null,Me=!!n.rebase_run&&I?.state==="OPEN",Ie=(n.rebased||[]).at(-1),re=f.filter(k=>k.started&&!k.done&&Q(k)),Ze=!!X&&bt(vt(f));let A=vn(n,I,re.some(bt),Ze,Y||z,!!j,me,Me,re.length>0||!!X);j&&A==="todo"&&(A="progress");const We=A==="review"&&!!te&&h-Date.parse(te)>ht,ae=!!Re&&Je&&A==="done";ae&&(A="review");let D=$n(A,I,u,!!X&&A!=="done",Y,me,{held:z,needsApproval:ve.length>0});const Fe=D==="builder"&&I?.state==="OPEN"&&!I.isDraft,qe=A==="progress"&&!D&&I?.state==="OPEN"&&(!I.isDraft||me),xe=ae||!!j&&A==="progress"&&!D&&!qe&&!be&&!H;xe&&(D="owner");const o=!!j&&f.some(k=>Q(k)&&String(k.started||"")>String(j)),g=A==="progress"&&!D&&(qe||!!j&&!o);g&&(A="review");const E=i&&String(i.ts)>String(n.held_at||""),$=A==="todo"&&(E?!!i.data.hold:!!n.held),y=$&&(E?i.data.until:n.pick_up_at)||null,M=A!=="done",B=!!b&&!(we&&String(b)<=we),Ke=v.filter(k=>(k.who||"claude")==="claude"&&!["status","command",Ee].includes(k.kind)&&!Cn.includes(k.line)).map(k=>String(k.ts)).sort().pop()||"",le=[oe,z&&n.merge_ready.tapped].filter(Boolean).map(String).sort().pop(),Ce=D==="merge"&&!!le&&le>Ke,Oe=v.filter(k=>(k.kind==="status"||k.kind===Qe)&&String(k.ts)>String(le)).sort((k,de)=>String(k.ts).localeCompare(String(de.ts))).at(-1),ne=Oe?.line==="land-waiting"?`${Ve.merging} \xB7 ${Oe.text[0].toLowerCase()}${Oe.text.slice(1)}`:Ve.merging,J=v.filter(k=>!["status",Ee].includes(k.kind)).sort((k,de)=>String(k.ts).localeCompare(String(de.ts))).at(-1),O=A==="todo"&&J?.line===wt?J.text:null,ie=j||f.filter(Q).map(k=>k.started).filter(Boolean).map(String).sort().pop()||null,Ge=I?.reviewDecision==="APPROVED",Kt=Y&&D==="owner"?`merge ${Y.branch} into ${l}`:z&&D==="merge"?Ce?ne:Bn(l):D==="merge"&&!ke?Ce?ne:(Ge?Ve:ot).waiting:D==="merge"&&!Ge?ot.auto:An[D],Gt=G===V.draft?`Review draft PR #${F[ee].number}`:null,Qt=!Me&&Ie?.to&&Ie.to===I?.headRefOid?`Rebased onto ${Ie.base||l}, pushed${I.mergeable==="MERGEABLE"?" \xB7 PR is clean again":""}`:null,Ht=Me&&D==="checking"?f.flatMap(k=>k.calls).sort((k,de)=>String(k.at).localeCompare(String(de.at))).at(-1)?.text??null:null;return{key:Se,kind:"work",order:s,file:e,fileTitle:t.title||e,id:n.id,ref:nt(t,n,K),title:n.title,storedLane:ze,created:n.created||null,reviewSince:g&&j||n.review_since||te||n.merge_ready?.at||n.pushed?.at||n.created||null,progressSince:ie,closedAt:A==="done"?N:null,internal:t.internal===!0,repo:t.internal===!0&&!t.repo?null:a,lane:A,waitsOn:D,builder:D==="checking"?null:u,reachedPhase:m,agents:f,worktree:p??null,eta:w,working:x,reopened:!!ye,answered:!!Le,awaitsDone:xe,ownerStep:xe?Re:null,ownerCheck:xe&&Re&&n.owner_check?{words:cn(un(n.owner_check.check)),broken:n.owner_check.broken||null}:null,stagedAt:A==="todo"&&n.phase==="staging"&&n.staged_at||null,roundAt:n.round_at||null,deps:n.deps||[],blockedUntil:n.blocked_until||null,blockReason:n.block_reason||null,held:$,heldUntil:y,risky:!!n.risky,bumpable:M,bumpedAt:M&&B?b:null,holdable:A!=="done",hasNotes:!!(n.plan||String(n.notes||"").trim()),finishable:A!=="done",merging:Ce,refusal:O,manualSteps:n.manual_steps||[],fromAsk:n.from_ask||null,unfiled:!!n.unfiled,filedHeld:!!n.filed_held,next:Fe||A==="done"||D==="checking"?null:g&&be?Rn:qe?_n:Qt||Kt||n.next||Gt||null,rebaseFailed:Ht,branch:n.branch||I?.headRefName||Y?.branch||null,pushed:A==="done"||ae?null:Y,mergeReady:A==="review"?z:null,stale:A!=="done"&&(We||!!u?.stale),stalled:n.stalled?.reason||null,landStopped:A==="review"&&z&&n.merge_ready.stopped&&String(n.merge_ready.stopped)>String(n.merge_ready.tapped||"")?{words:At("land"),at:n.merge_ready.stopped}:null,ticket:K?.source==="linear"&&!n.ticket&&n.linear?.length?null:Sn(K,n.ticket,{repo:a}),ticketSource:K?.source||null,linear:(n.linear||[]).map(k=>K?.source==="linear"&&pt(K,k,{repo:a})||yt(k,K)),pr:F[ee]||null,prs:F,reviewers:ve.map(k=>k.name),review:En(ve,I||r[ee]?.kept,Pe),inReviewSince:te,threadCount:v.length+_.length,lastTs:Ue,othersAt:je,grilling:Bt("grill",_,f,C),prototyping:Bt("prototype",_,f,C),storedPhase:n.phase||null,phase:null,recheckAt:n.recheck_at||null,updatedAt:I?.mergedAt||I?.updatedAt||r[ee]?.merged_at||u?.at||Ue||null,ask:null,fold:null,action:null,install:A==="done"?null:Ln(n.install,I),waitFacts:Dn(n,{agents:f,thread:v,events:_,refusedAt:O?J.ts:null,replyAt:g?Te:null})}}d(Nn,"workItem");function Dn(e,{agents:t,thread:n,events:s,refusedAt:r,replyAt:a}){const c=new Set(n.filter(f=>f.kind===Ne.answer).map(f=>f.ask)),l=n.filter(f=>f.kind===Ne.ask&&!c.has(f.ask)).sort((f,p)=>String(f.ts).localeCompare(String(p.ts)))[0],u=d(f=>s.filter(p=>at(p,f)).pop()?.ts||null,"requestAt"),m=n.filter(f=>(f.who||"claude")==="claude"&&!["status","agent","command",Ee].includes(f.kind)&&f.line!==wt).map(f=>f.ts);return{handedBackAt:st(t),next:e.next||null,ask:l?{at:l.ts,text:l.text}:null,grillAt:u("grill"),prototypeAt:u("prototype"),refusedAt:r,replyAt:a,unblockedAt:e.unblocked_at||null,unverified:t.some(f=>f.calls.some(p=>!f.verified||String(p.at)>String(f.verified.at))),writes:[...m,...t.flatMap(f=>[f.verified?.at,f.started])].filter(Boolean).map(String).sort()}}d(Dn,"waitFactsOf");function Ln(e,t){if(!e?.url)return null;const n=e.sha?String(e.sha).toLowerCase():null,s=t?.headRefOid?String(t.headRefOid).toLowerCase():null;return{url:e.url,label:e.label||null,builtAt:e.built_at||null,sha:n,older:!!(n&&s&&!s.startsWith(n))}}d(Ln,"installOf");function Tn(e,{needsApproval:t=!0}={}){if(e?.state!=="OPEN")return[`PR is ${String(e?.state||"unknown").toLowerCase()}`];const{state:n,pending:s,failing:r}=e.checks||{};return[e.isDraft&&"it is a draft",!xt(e,t)&&`review: ${String(e.reviewDecision||"none").toLowerCase().replace(/_/g," ")}`,n==="fail"&&`failing: ${r?.length?r.join(", "):"a check"}`,n==="pending"&&`pending: ${s?se(s,"check"):"checks"}`,e.mergeStateStatus!=="CLEAN"&&`merge state: ${String(e.mergeStateStatus||"unknown").toLowerCase()}`].filter(Boolean)}d(Tn,"mergeBlockers");const Pn=d((e,t)=>!Tn(e,{needsApproval:t}).length,"mergeable");function Un(e,t,n){if(pe(e))return null;const s=`#selectedCard=${encodeURIComponent(e.key)}`;if(e.stalled)return{kind:q.stalled,url:s,label:`stalled: ${e.stalled}`};if(jn(e)){const r=t.get(e.questions.first);return De.test(r?.url||"")?{kind:q.pick,url:`questions.html#${r.id}`,label:"Prototypes ready: pick a take"}:{kind:q.questions,url:`questions.html#${e.questions.first}`,label:`Answer ${se(e.questions.open,"question")}`}}return e.waitsOn==="ready"?{kind:q.ready,url:e.pr.url,label:`Review draft PR #${e.pr.number}`}:e.waitsOn==="merge"&&e.manualSteps.length&&!e.mergeReady?{kind:q.steps,url:s,label:"Run the manual steps, then merge"}:e.waitsOn==="merge"&&(!n||e.mergeReady)&&!e.merging&&e.prs.every(r=>["MERGED","CLOSED"].includes(r.state)||Pn(r,e.reviewers.length>0))?{kind:q.merge,url:s,label:"Merge"}:null}d(Un,"actionOf");const jn=d(e=>!!(e.questions&&e.questions.state!=="resolved"&&e.questions.open>0),"asking"),rt=new Set([q.pick,q.questions]);function Ot(e){return e.questions?ce.openQuestions:null}d(Ot,"actionButtonOf");const Wn=d(e=>e.kind==="work"&&e.lane==="todo"&&!e.action&&!e.unpicked,"buildableOf"),Fn=d((e,t)=>/^item:/.test(e.topic||"")&&De.test(e.url||"")&&!(e.options||[]).some(n=>n.id===t),"picksNoTake");function Kn(e,t){const n=e.filter(a=>a.topic===`item:${t}`),r=[...n.filter(a=>De.test(a.url||"")).flatMap(a=>a.answers.map(c=>({q:a,a:c,again:Fn(a,c.choice)}))),...n.filter(a=>a.request).map(a=>({q:a,a:a.request,again:!0}))].sort((a,c)=>String(a.a.ts).localeCompare(String(c.a.ts))).at(-1);return r?.again?{id:r.q.id,note:r.a.note.replace(/\s+/g," ").trim()}:null}d(Kn,"noTakePicked");const Gn="Prototype again",Qn=Object.freeze({[q.pick]:ce.openQuestions,[q.questions]:ce.openQuestions,[q.ready]:ce.github,[q.steps]:ce.github,[q.merge]:ce.merge,[q.stalled]:ce.reply}),Hn="Merges once unblocked",Vn=d(e=>`Blocked ${[e.blockedBy.length&&`by ${e.blockedBy.map(t=>t.ref).join(", ")}`,e.blockedUntil&&`until ${e.blockedUntil}`].filter(Boolean).join(" and ")}`,"mergeBlockedWords");function Xn(e,t,n){if(e.action=Un(e,t,n),e.mergeBlocked=e.action?.kind===q.merge&&e.blocked?Vn(e):null,e.mergeBlocked&&Object.assign(e,{action:null,next:Hn}),e.action?.kind===q.questions&&e.questions.fromAsks){Object.assign(e,{action:null,waitsOn:"owner",next:e.action.label,actionButton:Ot(e),resolvedBy:null});return}e.awaitsDone&&=!e.action,e.awaitsDone||Object.assign(e,{ownerStep:null,ownerCheck:null}),e.actionButton=Ot(e),e.resolvedBy=Qn[e.action?.kind]||null,e.action?.kind===q.merge&&!e.mergeReady&&(e.next=(e.pr?.reviewDecision==="APPROVED"?Ve:ot).ready),e.awaitsDone&&Object.assign(e,{lane:"review",next:e.ownerStep||Mn,bumpable:!1,bumpedAt:null});const s=e.lane==="todo"||e.waitsOn==="ready"||e.lane==="progress"&&e.waitsOn==="owner";if(!e.action||!s||e.stalled)return;const r=e.lane==="todo"&&rt.has(e.action.kind)?"todo":"review";Object.assign(e,{lane:r,waitsOn:e.waitsOn==="ready"?"ready":"owner",next:e.action.label})}d(Xn,"placeOwnerWait");const Et=d((e,t)=>e?e!=="todo":t.some(Q),"started"),$t="none set yet";function So(e,t){const n=t.filter(s=>s?.started);return e.next||et(e)||e.pushed||e.held||e.lane==="done"||st(n)?!1:!n.some(s=>!s.done)&&Et(e.lane,n)}d(So,"owesNext");const fe="visual",$e="helper",Yn=new Set(["grill","prototype",fe]),Z=d(e=>Yn.has(e.kind),"plans"),Q=d(e=>!Z(e),"builds"),zn={grill:"grilling",prototype:"prototyping"},Jn=d((e,t)=>`${e} ${t} by orchestrator`,"orchestratorDid"),At=d(e=>Jn(e,"stopped"),"stoppedWords"),Zn={grill:"grill-more",prototype:"prototype-request"},at=d((e,t)=>e.kind===Zn[t]&&!e.data?.question,"isRequest");function Bt(e,t,n,s){if(n.some(a=>a.kind===e&&a.started&&!a.done))return!0;const r=t.filter(a=>at(a,e)).pop();return!!(r&&!r.reply&&!s.some(a=>String(a)>String(r.ts))&&!n.some(a=>a.kind===e&&a.done&&String(a.started)>=String(r.ts)))}d(Bt,"planningOf");const vo=30*6e4;function xo(e,t){const n=[];for(const s of e.items){if(pe(s)||(s.agents||[]).some(a=>a.kind==="prototype"&&a.started&&!a.done))continue;const r=s.prototyping&&t.filter(a=>a.data?.key===s.key&&at(a,"prototype")).pop();r&&n.push({key:s.key,request:r.id,since:r.ts});for(const a of e.owner.filter(c=>c.topic===`item:${s.key}`&&c.state==="requested"))n.push({key:s.key,request:a.request.id,question:a.id,since:a.request.ts})}return n}d(xo,"prototypeWaits");function lt(e={},t=!1){return Object.entries(e).filter(([n,s])=>s&&typeof s=="object"&&(t||!n.startsWith("past-"))).map(([n,s])=>({name:n,kind:_t(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,log:s.session?.log||null,pid:s.session?.pid??null,calls:Array.isArray(s.calls)?s.calls:[],verified:s.verified||null,stopped:s.stopped||null,handed_back:s.handed_back||null,...s.pause?{pause:s.pause}:{},...s.kind===$e?{writes:!!s.writes,requested:s.requested||null}:{},...s.waits?{waits:s.waits}:{}})).sort((n,s)=>String(n.started||"").localeCompare(String(s.started||""))||n.name.localeCompare(s.name))}d(lt,"agentsOf");function it(e={}){return Object.values(e).filter(t=>t?.worktree?.path).sort((t,n)=>String(t.started||"").localeCompare(String(n.started||""))).at(-1)?.worktree.path??null}d(it,"worktreeOf");function es(e,t=[]){const n=e.filter(Q).map(s=>String(s.started||"")).filter(Boolean).sort().pop();return n===void 0?null:t.filter(s=>s.kind==="status"&&s.phase&&!s.planner&&String(s.ts)>=n).sort((s,r)=>String(s.ts).localeCompare(String(r.ts))).pop()?.phase||null}d(es,"reachedPhaseOf");function ts(e,t=[],n,s=null,r=null,a=!1){const c=e.filter(b=>b.started&&!b.done&&b.kind!==fe),l=c.every(Z),u=l?c:c.filter(Q),m=u.map(b=>String(b.started||"")).sort()[0];if(m===void 0)return null;const f=t.filter(b=>b.kind==="status"&&String(b.ts)>=m&&(l||!b.planner)).sort((b,i)=>String(b.ts).localeCompare(String(i.ts))).pop(),p=u.filter(b=>b.kind!==$e).at(-1),w=u.some(b=>b.kind===ue.install)?ue.install:p?_t(p.kind):"claude",v=l?null:u.find(b=>b.stopped&&String(b.stopped.at)>=String(b.started))?.stopped,_=f?.ts||m,x=v?v.at:s!==null&&s>Date.parse(_)?new Date(s).toISOString():_,P=f?f.phase||f.planner?f.text.replace(": "," \xB7 "):f.text||null:l?zn[w]:null,C=n-Date.parse(x),L=l?null:u.find(b=>b.pause)?.pause??null,T=!l&&!v&&(r==="held"||!!L?.paused),W=l||T?null:v?!1:r,h=T?!1:W===null?C>=mn:!W;return{progress:P,phase:f?.phase||null,at:_,kind:w,planner:l,silentSince:x,stale:h,alive:W,held:T,quiet:!!L||W===!0&&!a&&C>=wn,stopped:v?At(v.run):null,...L?{pause:{cause:L.cause,words:L.words,paused:!!L.paused}}:{}}}d(ts,"builderOf");const Oo={blocked:["nothing blocks it","nothing blocks it"],held:["you unhold it","unheld"],usage:["the usage pause lifts","the usage pause lifted"],bumped:["a slot frees","a slot freed"]},ns=15*6e4,ss=d(e=>(e.agents||[]).find(t=>t.name==="main"&&t.started&&!t.done&&Q(t))??null,"mainBuild"),os=d((e,t,n,{now:s,usageHeld:r})=>e.blocked||e.held||r(n??"claude")||!!(t.until&&s<Date.parse(t.until)),"stillPaused");function Eo(e,{now:t=Date.now(),usageHeld:n}){const s={ask:[],drop:[],stop:[],resume:[]},r=[];for(const a of e){if(a.kind!=="work"||a.lane!=="progress")continue;const c=ss(a);if(!c)continue;const{pause:l}=c;if(!l){a.builder?.phase!=="landing"&&n(c.kind??"claude")&&s.ask.push(a.key);continue}const u=os(a,l,c.kind,{now:t,usageHeld:n});if(l.paused){u||r.push({key:a.key,at:l.paused});continue}!u&&l.cause!=="bumped"?s.drop.push(a.key):t-Date.parse(l.at)>=ns&&s.stop.push(a.key)}return s.resume=r.sort((a,c)=>String(a.at).localeCompare(String(c.at))).map(a=>a.key),s}d(Eo,"pauseWork");const dt={opus:"Claude Opus",sonnet:"Claude Sonnet",codex:"Codex"},rs=d(({model:e,kind:t})=>dt[e]||dt[t]||(t==="claude"?dt.opus:null),"modelName"),_t=d(e=>e==="opus"||e==="sonnet"?"claude":e,"providerOf");function Ae(e){const t=rs(e),{kind:n}=e,s=e.label?.replace(/\bbuilder\b/,"implementer").replace(/\bBuilder\b/,"Implementer"),r=d(c=>c[0].toLowerCase()+c.slice(1),"lower");if(t&&Z(e))return`${t} ${n}`;if(t)return`${t} ${r(s?.replace(/^(?:claude\s+)?(?:opus|sonnet|codex)\b\s*/i,"")||"implementer")}`;const a=n?n[0].toUpperCase()+n.slice(1):"";return a?s?s.toLowerCase().includes(n.toLowerCase())?s:`${a} ${r(s)}`:a:s||"Implementer"}d(Ae,"agentName");const Rt=d((e,t)=>!!e.resolved&&(!e.resolved_at||String(e.resolved_at)>=String(t)),"resolvedSince");function as(e,t,n=[],s=[],r=[]){const a=t.filter(x=>x.data?.id===e.id),c=a[a.length-1]||null,l=r.filter(x=>x.data?.question===e.id).at(-1),u=d((x,...P)=>!!x&&P.every(C=>String(x.ts)>String(C||"")),"after"),m=!!(e.resolved&&e.resolved_at&&c&&String(c.ts)>e.resolved_at),f=n.filter(x=>x.data?.question===e.id&&u(x,c?.ts,l?.ts)).at(-1),p=u(l,c?.ts,f?.ts,e.resolved_at),w=e.resolved&&!m&&!p,v=w?s.filter(x=>x.data?.question===e.id&&String(x.ts)>String(e.resolved_at||"")).at(-1):null,_=p?"open":w?"resolved":f?"requested":c?c.reply?"replied":"answered":"open";return{...e,state:_,reopened:p,request:f?{id:f.id,ts:f.ts,note:f.data.note||"",choice:f.data.choice||null}:null,reask:v?{id:v.id,ts:v.ts}:null,answers:a.map(x=>({ts:x.ts,choice:x.data.choice||null,note:x.data.note||"",files:x.data.files||[],reply:x.reply,replies:x.replies,acted:Rt(e,x.ts)}))}}d(as,"ownerQuestion");const ge=d(e=>e.round??(De.test(e.url||"")?1:0),"roundOf");function ls(e,t){const n=d((s,r)=>s.confirm_of||(s.options||[]).some(a=>a.id===r.choice),"settles");return e.filter(s=>ge(s)===t&&s.answers.length).map(s=>[s,s.answers.at(-1)]).filter(([s,r])=>n(s,r)).map(([,s])=>s).sort((s,r)=>String(s.ts).localeCompare(String(r.ts))).at(-1)}d(ls,"roundSettledBy");function is(e){const t=e.find(r=>r.request&&!e.some(a=>ge(a)>ge(r)));if(t)return{n:ge(t)+1,state:"making"};const n=Math.max(0,...e.map(ge));if(!n)return null;const s=ls(e,n);return s?{n,state:"closed",take:s.choice}:e.some(r=>ge(r)===n&&r.state!=="resolved")?{n,state:"back"}:null}d(is,"roundState");function ds(e){const t=e.filter(n=>De.test(n.url||"")).map(n=>({n:ge(n),q:n.id,url:n.url,confirm:!!n.confirm_of,state:n.state})).sort((n,s)=>n.n-s.n);return t.length?{open:t.some(n=>n.state==="open"),rounds:t}:null}d(ds,"prototypesOf");const Be=d((e,t,n)=>{const s=e.get(t);s?s.push(n):e.set(t,[n])},"add"),_e=d((e,t)=>{const n=new Map;for(const s of e)Be(n,t(s),s);return n},"groupBy");function cs(e,t,n=new Set){const s=new Set(e.map(c=>c.id)),r=[...e];t.some(c=>!s.has(c.topic))&&r.push({id:"",title:"Other"});const a=_e(t,c=>s.has(c.topic)?c.topic:"");return r.map(c=>{const l=a.get(c.id)||[],u=d(p=>l.filter(w=>w.state===p).length,"count"),m=l.every(p=>p.state==="resolved")||n.has(c.id)&&!u("open"),f=l.length&&m?"resolved":"open";return{...c,state:f,questions:l.map(p=>p.id),open:u("open"),answered:u("answered"),replied:u("replied"),resolved:u("resolved"),round:is(l)}}).filter(c=>c.questions.length)}d(cs,"ownerTopics");const Xe={implement:"Implementation requested",prototype:"Prototype requested",grill:"Grill requested",visual:"Visual requested",reask:"Re-ask requested",hold:"Held",resume:"Resumed",done:"Marked done",staged:"Marked as staging",readied:"Marked as ready",related:"Related to",unrelated:"Unrelated from",followup:"Follow-up","followup-of":"Follow-up of"},Mt={"grill-more":"grill","prototype-request":"prototype","visual-request":"visual","reask-request":"reask"},It={"mark-ready":"Mark ready",...Object.fromEntries(Object.entries(Mt).map(([e,t])=>[e,Xe[t]]))},us=["grill-more","prototype-request","visual-request"],ps="answers submitted",qt={reply:"Reply",build:"Implement"},fs=d(e=>qt[e.data.intent],"intentLabel"),gs=d((e,t)=>{if(e.kind!=="prototype-request"||!t.has(e.data.question))return e.data.text||(e.data.files?.length?"":R(It,e.kind))||"";const n=t.get(e.data.question),s=(n.options||[]).find(r=>r.id===e.data.choice);return[Nt(t,n),s&&`${s.id}. ${s.label}`,e.data.note].filter(Boolean).join(`
1
+ var Vt=Object.defineProperty;var d=(e,t)=>Vt(e,"name",{value:t,configurable:!0});import{routeRecord as R}from"../hub/routes.mjs";import Xt from"node:fs";import{ticketBadge as pt,ticketIn as Yt}from"./tickets.mjs";import{prLinks as zt,reviewLinks as Jt,reviewTime as Zt,reviewersOf as en}from"./review.mjs";import{remoteFromInput as tn}from"../hub/repos.mjs";import{reviewsOf as nn,reviewNeeds as sn,reviewActions as on}from"./auto-review.mjs";import{cardPRs as rn,hasPR as et,leastReady as an,prKeyOf as ln,prURL as dn}from"../shared/card-prs.mjs";import ft from"node:vm";import{checkWords as cn,parseCheck as un}from"../shared/owner-check.mjs";import{buildScriptOf as pn}from"./build-script.mjs";import{HUB as Qe,hubLineOf as gt}from"./hub-lines.mjs";const fn="card-merged",tt=ft.createContext({});for(const e of["ui-enums.js","ui-order.js"])ft.runInContext(Xt.readFileSync(new URL(`./public/${e}`,import.meta.url),"utf8"),tt);const{BUTTON:ce,ACTION_KIND:q,BUILDER_KIND:ue,PR_STATE:V,PHASES:kt}=tt.BoardUI,{laneOrder:gn}=tt.BoardUI,kn=["item-message","mark-ready","grill-more","prototype-request","visual-request","reask-request"],mt="question-reopen",bo=Object.freeze({todo:"Todo",progress:"In Progress",review:"In Review",done:"Done"}),Ee="move",mn=10*6e4,wn=5*6e4,wt="build-refused",Ne=Object.freeze({ask:"builder-ask",answer:"builder-answer"}),hn=864e5,pe=d(e=>e.lane==="done","closedLane"),yn=d(e=>`file:${e}`,"fileFold"),ht=7*hn,yt=d((e,t)=>({id:e,url:t?.workspace?`https://linear.app/${t.workspace}/issue/${e}`:null}),"linearRef"),bn=d(e=>String(e).replace(/^S(\d+)$/,"INT-$1"),"internalRef"),nt=d((e,t,n)=>Yt(n,t.ticket)?t.ticket:e.internal===!0?bn(t.id):t.id,"refOf"),Sn=d((e,t,n)=>pt(e,t,n)||(t?{id:String(t),url:null,source:null}:null),"ticketOf");function vn(e,t,n,s,r,a,c,l,u){return!a&&(e.lane==="done"||t?.state==="MERGED"||t?.state==="CLOSED")?"done":(n||s)&&!l?"progress":r?"review":t?.state==="OPEN"?t.checks?.state==="fail"&&(!t.isDraft||c)?"progress":"review":t||u?"progress":"todo"}d(vn,"laneOf");const He="Lander",bt=d(e=>e.kind!==ue.install&&e.label!==He,"changesCode");function xn(e,t){return e?.state==="MERGED"||t?V.merged:e?.state==="CLOSED"?V.closed:e?.state==="OPEN"?e.isDraft?V.draft:V.open:null}d(xn,"prStateOf");const St=d(e=>e.pr?.status===V.closed,"unlanded"),On=d(e=>e.some(t=>t.kind==="work")&&e.every(t=>t.lane==="done"),"finishedOf");function st(e){const t=vt(e);return t?.done&&t.handed_back?t.handed_back:null}d(st,"handedBackAt");const vt=d(e=>e.filter(t=>t?.started&&Q(t)&&t.kind!==ue.install).sort((t,n)=>String(t.started).localeCompare(String(n.started))).at(-1),"newestBuild");function En(e,t,n){return Object.fromEntries(e.map(s=>{const r=R(t?.reviews,s.github),a=r==="APPROVED"?"approved":r==="CHANGES_REQUESTED"?"changes":Object.hasOwn(n,s.name)?"sent":"unsent";return[s.name,{state:a,since:a==="sent"?R(n,s.name):null,github:s.github||null}]}))}d(En,"reviewOf");function $n(e,t,n,s,r,a,{held:c,needsApproval:l}){return n?.kind===ue.install?"builder":s?"checking":n?"builder":c&&e==="review"?"merge":r&&e==="review"?"owner":e==="review"&&t?.state==="OPEN"&&t.isDraft?a?"ci":"ready":e==="review"?xt(t,l)?"merge":"reviewers":null}d($n,"waitsOnOf");const xt=d((e,t)=>e?.reviewDecision==="APPROVED"||!t&&!["REVIEW_REQUIRED","CHANGES_REQUESTED"].includes(e?.reviewDecision),"reviewed"),De=/(^|\/)prototype-[\w-]+\.html/,An={reviewers:"Waiting for PR reviewers",merge:"Approved, merges once CI and the merge state allow",ci:"Marked ready for review once CI passes"},Ve={waiting:"Approved, Merge shows once CI and the merge state allow",ready:"Approved and green: tap Merge",merging:"Merging"},ot={auto:"Ready, merges once CI and the merge state allow",waiting:"Ready, Merge shows once CI and the merge state allow",ready:"Ready and green: tap Merge"},Bn=d(e=>`Verified: tap Merge to land it on ${e}`,"heldNext"),_n="Fix the failing CI",Rn="Your reply waits on the orchestrator",Mn="Tap Done",In=["status","agent","ask-link","command",Ee,Ne.answer],qn=d(e=>(e.who==="claude"||e.via==="github")&&!In.includes(e.kind)&&(e.kind!==Qe||gt(e).fault),"countsUnread"),Cn=["lander-handoff","land-waiting"];function Nn({slug:e,itemFile:t,it:n,order:s,prs:r,repo:a,remote:c,base:l,builder:u,reachedPhase:m,agents:f,worktree:p,eta:w,thread:v,events:_,working:x,said:P,askedAt:C,topicItems:L,prSlack:T,boardReviewers:W,now:h,bumpedAt:b,holdAsk:i,closedAt:S,mergeAt:oe,autoMerge:ke,skipDraft:me,tickets:K}){const F=r.map(k=>({repo:k.repo,number:k.number,url:dn(k),...k.gh||k.kept||{},status:xn(k.gh,k.merged_at)})),ee=an(r.map(k=>k.gh)),I=r[ee]?.gh||null,G=F[ee]?.status??null,ze=n.lane||null,U=!!S&&String(S)>String(n.lane_at||"");U&&(n={...n,lane:"done",closed_at:S,reopened_at:void 0});const N=[n.closed_at,...r.flatMap(k=>[k.merged_at,k.gh?.mergedAt])].filter(Boolean).map(String).sort().pop()||null,we=String(N||""),he=F.length>0&&F.every(k=>k.status===V.closed),ye=(n.lane==="done"||n.reopened_at||G===V.merged)&&!he&&([...P.filter(k=>k.kind==="item-message").map(k=>k.ts),...C].map(String).filter(k=>k>we).sort().pop()||n.reopened_at)||void 0,Le=n.answered_at&&!n.phase&&!n.closed_at&&!ye?String(n.answered_at):void 0,j=ye||Le,Re=n.owner_step&&!U?String(n.owner_step):null,Je=(n.lane==="done"||G===V.merged)&&!he,H=!!j&&!!n.next&&String(n.next_at||"")>String(n.reopened_at||N||"");j&&!H&&(n={...n,next:void 0});const X=st(f),Te=j&&P.filter(k=>String(k.ts)>we&&(k.state==="waiting"||k.state==="working")).map(k=>String(k.ts)).sort()[0]||null,be=!!Te,Se=`${e}/${n.id}`,ve=en(t,W),Pe=Object.fromEntries(Object.entries(Jt(t,n,Se,L,zt(T,r))).map(([k,de])=>[k,Zt(de.ts)])),te=Object.values(Pe).filter(Boolean).sort()[0]||null,Ue=[...v.map(k=>k.ts),..._.map(k=>k.ts)].sort().pop()||null,je=[...v.filter(k=>qn({...k,who:k.who||"claude"})).map(k=>k.ts),..._.map(k=>k.reply?.at)].filter(Boolean).sort(),Y=!et(n)&&n.pushed?.branch?{branch:n.pushed.branch,at:n.pushed.at||null,host:c?.host||null}:null,z=!et(n)&&n.merge_ready?.branch?{branch:n.merge_ready.branch,at:n.merge_ready.at||null,base:l}:null,Me=!!n.rebase_run&&I?.state==="OPEN",Ie=(n.rebased||[]).at(-1),re=f.filter(k=>k.started&&!k.done&&Q(k)),Ze=!!X&&bt(vt(f));let A=vn(n,I,re.some(bt),Ze,Y||z,!!j,me,Me,re.length>0||!!X);j&&A==="todo"&&(A="progress");const We=A==="review"&&!!te&&h-Date.parse(te)>ht,ae=!!Re&&Je&&A==="done";ae&&(A="review");let D=$n(A,I,u,!!X&&A!=="done",Y,me,{held:z,needsApproval:ve.length>0});const Fe=D==="builder"&&I?.state==="OPEN"&&!I.isDraft,qe=A==="progress"&&!D&&I?.state==="OPEN"&&(!I.isDraft||me),xe=ae||!!j&&A==="progress"&&!D&&!qe&&!be&&!H;xe&&(D="owner");const o=!!j&&f.some(k=>Q(k)&&String(k.started||"")>String(j)),g=A==="progress"&&!D&&(qe||!!j&&!o);g&&(A="review");const E=i&&String(i.ts)>String(n.held_at||""),$=A==="todo"&&(E?!!i.data.hold:!!n.held),y=$&&(E?i.data.until:n.pick_up_at)||null,M=A!=="done",B=!!b&&!(we&&String(b)<=we),Ke=v.filter(k=>(k.who||"claude")==="claude"&&!["status","command",Ee].includes(k.kind)&&!Cn.includes(k.line)).map(k=>String(k.ts)).sort().pop()||"",le=[oe,z&&n.merge_ready.tapped].filter(Boolean).map(String).sort().pop(),Ce=D==="merge"&&!!le&&le>Ke,Oe=v.filter(k=>(k.kind==="status"||k.kind===Qe)&&String(k.ts)>String(le)).sort((k,de)=>String(k.ts).localeCompare(String(de.ts))).at(-1),ne=Oe?.line==="land-waiting"?`${Ve.merging} \xB7 ${Oe.text[0].toLowerCase()}${Oe.text.slice(1)}`:Ve.merging,J=v.filter(k=>!["status",Ee].includes(k.kind)).sort((k,de)=>String(k.ts).localeCompare(String(de.ts))).at(-1),O=A==="todo"&&J?.line===wt?J.text:null,ie=j||f.filter(Q).map(k=>k.started).filter(Boolean).map(String).sort().pop()||null,Ge=I?.reviewDecision==="APPROVED",Kt=Y&&D==="owner"?`merge ${Y.branch} into ${l}`:z&&D==="merge"?Ce?ne:Bn(l):D==="merge"&&!ke?Ce?ne:(Ge?Ve:ot).waiting:D==="merge"&&!Ge?ot.auto:An[D],Gt=G===V.draft?`Review draft PR #${F[ee].number}`:null,Qt=!Me&&Ie?.to&&Ie.to===I?.headRefOid?`Rebased onto ${Ie.base||l}, pushed${I.mergeable==="MERGEABLE"?" \xB7 PR is clean again":""}`:null,Ht=Me&&D==="checking"?f.flatMap(k=>k.calls).sort((k,de)=>String(k.at).localeCompare(String(de.at))).at(-1)?.text??null:null;return{key:Se,kind:"work",order:s,file:e,fileTitle:t.title||e,id:n.id,ref:nt(t,n,K),title:n.title,storedLane:ze,created:n.created||null,reviewSince:g&&j||n.review_since||te||n.merge_ready?.at||n.pushed?.at||n.created||null,progressSince:ie,closedAt:A==="done"?N:null,internal:t.internal===!0,repo:t.internal===!0&&!t.repo?null:a,lane:A,waitsOn:D,builder:D==="checking"?null:u,reachedPhase:m,agents:f,worktree:p??null,eta:w,working:x,reopened:!!ye,answered:!!Le,awaitsDone:xe,ownerStep:xe?Re:null,ownerCheck:xe&&Re&&n.owner_check?{words:cn(un(n.owner_check.check)),broken:n.owner_check.broken||null}:null,stagedAt:A==="todo"&&n.phase==="staging"&&n.staged_at||null,roundAt:n.round_at||null,deps:n.deps||[],blockedUntil:n.blocked_until||null,blockReason:n.block_reason||null,held:$,heldUntil:y,risky:!!n.risky,bumpable:M,bumpedAt:M&&B?b:null,holdable:A!=="done",hasNotes:!!(n.plan||String(n.notes||"").trim()),finishable:A!=="done",merging:Ce,refusal:O,manualSteps:n.manual_steps||[],fromAsk:n.from_ask||null,unfiled:!!n.unfiled,filedHeld:!!n.filed_held,next:Fe||A==="done"||D==="checking"?null:g&&be?Rn:qe?_n:Qt||Kt||n.next||Gt||null,rebaseFailed:Ht,branch:n.branch||I?.headRefName||Y?.branch||null,pushed:A==="done"||ae?null:Y,mergeReady:A==="review"?z:null,stale:A!=="done"&&(We||!!u?.stale),stalled:n.stalled?.reason||null,landStopped:A==="review"&&z&&n.merge_ready.stopped&&String(n.merge_ready.stopped)>String(n.merge_ready.tapped||"")?{words:At("land"),at:n.merge_ready.stopped}:null,ticket:K?.source==="linear"&&!n.ticket&&n.linear?.length?null:Sn(K,n.ticket,{repo:a}),ticketSource:K?.source||null,linear:(n.linear||[]).map(k=>K?.source==="linear"&&pt(K,k,{repo:a})||yt(k,K)),pr:F[ee]||null,prs:F,reviewers:ve.map(k=>k.name),review:En(ve,I||r[ee]?.kept,Pe),inReviewSince:te,threadCount:v.length+_.length,lastTs:Ue,othersAt:je,grilling:Bt("grill",_,f,C),prototyping:Bt("prototype",_,f,C),storedPhase:n.phase||null,phase:null,recheckAt:n.recheck_at||null,updatedAt:I?.mergedAt||I?.updatedAt||r[ee]?.merged_at||u?.at||Ue||null,ask:null,fold:null,action:null,install:A==="done"?null:Ln(n.install,I),waitFacts:Dn(n,{agents:f,thread:v,events:_,refusedAt:O?J.ts:null,replyAt:g?Te:null})}}d(Nn,"workItem");function Dn(e,{agents:t,thread:n,events:s,refusedAt:r,replyAt:a}){const c=new Set(n.filter(f=>f.kind===Ne.answer).map(f=>f.ask)),l=n.filter(f=>f.kind===Ne.ask&&!c.has(f.ask)).sort((f,p)=>String(f.ts).localeCompare(String(p.ts)))[0],u=d(f=>s.filter(p=>at(p,f)).pop()?.ts||null,"requestAt"),m=n.filter(f=>(f.who||"claude")==="claude"&&!["status","agent","command",Ee].includes(f.kind)&&f.line!==wt).map(f=>f.ts);return{handedBackAt:st(t),next:e.next||null,ask:l?{at:l.ts,text:l.text}:null,grillAt:u("grill"),prototypeAt:u("prototype"),refusedAt:r,replyAt:a,unblockedAt:e.unblocked_at||null,unverified:t.some(f=>f.calls.some(p=>!f.verified||String(p.at)>String(f.verified.at))),writes:[...m,...t.flatMap(f=>[f.verified?.at,f.started])].filter(Boolean).map(String).sort()}}d(Dn,"waitFactsOf");function Ln(e,t){if(!e?.url)return null;const n=e.sha?String(e.sha).toLowerCase():null,s=t?.headRefOid?String(t.headRefOid).toLowerCase():null;return{url:e.url,label:e.label||null,builtAt:e.built_at||null,sha:n,older:!!(n&&s&&!s.startsWith(n))}}d(Ln,"installOf");function Tn(e,{needsApproval:t=!0}={}){if(e?.state!=="OPEN")return[`PR is ${String(e?.state||"unknown").toLowerCase()}`];const{state:n,pending:s,failing:r}=e.checks||{};return[e.isDraft&&"it is a draft",!xt(e,t)&&`review: ${String(e.reviewDecision||"none").toLowerCase().replace(/_/g," ")}`,n==="fail"&&`failing: ${r?.length?r.join(", "):"a check"}`,n==="pending"&&`pending: ${s?se(s,"check"):"checks"}`,e.mergeStateStatus!=="CLEAN"&&`merge state: ${String(e.mergeStateStatus||"unknown").toLowerCase()}`].filter(Boolean)}d(Tn,"mergeBlockers");const Pn=d((e,t)=>!Tn(e,{needsApproval:t}).length,"mergeable");function Un(e,t,n){if(pe(e))return null;const s=`#selectedCard=${encodeURIComponent(e.key)}`;if(e.stalled)return{kind:q.stalled,url:s,label:`stalled: ${e.stalled}`};if(jn(e)){const r=t.get(e.questions.first);return De.test(r?.url||"")?{kind:q.pick,url:`questions.html#${r.id}`,label:"Prototypes ready: pick a take"}:{kind:q.questions,url:`questions.html#${e.questions.first}`,label:`Answer ${se(e.questions.open,"question")}`}}return e.waitsOn==="ready"?{kind:q.ready,url:e.pr.url,label:`Review draft PR #${e.pr.number}`}:e.waitsOn==="merge"&&e.manualSteps.length&&!e.mergeReady?{kind:q.steps,url:s,label:"Run the manual steps, then merge"}:e.waitsOn==="merge"&&(!n||e.mergeReady)&&!e.merging&&e.prs.every(r=>["MERGED","CLOSED"].includes(r.state)||Pn(r,e.reviewers.length>0))?{kind:q.merge,url:s,label:"Merge"}:null}d(Un,"actionOf");const jn=d(e=>!!(e.questions&&e.questions.state!=="resolved"&&e.questions.open>0),"asking"),rt=new Set([q.pick,q.questions]);function Ot(e){return e.questions?ce.openQuestions:null}d(Ot,"actionButtonOf");const Wn=d(e=>e.kind==="work"&&e.lane==="todo"&&!e.action&&!e.unpicked,"buildableOf"),Fn=d((e,t)=>/^item:/.test(e.topic||"")&&De.test(e.url||"")&&!(e.options||[]).some(n=>n.id===t),"picksNoTake");function Kn(e,t){const n=e.filter(a=>a.topic===`item:${t}`),r=[...n.filter(a=>De.test(a.url||"")).flatMap(a=>a.answers.map(c=>({q:a,a:c,again:Fn(a,c.choice)}))),...n.filter(a=>a.request).map(a=>({q:a,a:a.request,again:!0}))].sort((a,c)=>String(a.a.ts).localeCompare(String(c.a.ts))).at(-1);return r?.again?{id:r.q.id,note:r.a.note.replace(/\s+/g," ").trim()}:null}d(Kn,"noTakePicked");const Gn="Prototype again",Qn=Object.freeze({[q.pick]:ce.openQuestions,[q.questions]:ce.openQuestions,[q.ready]:ce.github,[q.steps]:ce.github,[q.merge]:ce.merge,[q.stalled]:ce.reply}),Hn="Merges once unblocked",Vn=d(e=>`Blocked ${[e.blockedBy.length&&`by ${e.blockedBy.map(t=>t.ref).join(", ")}`,e.blockedUntil&&`until ${e.blockedUntil}`].filter(Boolean).join(" and ")}`,"mergeBlockedWords");function Xn(e,t,n){if(e.action=Un(e,t,n),e.mergeBlocked=e.action?.kind===q.merge&&e.blocked?Vn(e):null,e.mergeBlocked&&Object.assign(e,{action:null,next:Hn}),e.action?.kind===q.questions&&e.questions.fromAsks){Object.assign(e,{action:null,waitsOn:"owner",next:e.action.label,actionButton:Ot(e),resolvedBy:null});return}e.awaitsDone&&=!e.action,e.awaitsDone||Object.assign(e,{ownerStep:null,ownerCheck:null}),e.actionButton=Ot(e),e.resolvedBy=Qn[e.action?.kind]||null,e.action?.kind===q.merge&&!e.mergeReady&&(e.next=(e.pr?.reviewDecision==="APPROVED"?Ve:ot).ready),e.awaitsDone&&Object.assign(e,{lane:"review",next:e.ownerStep||Mn,bumpable:!1,bumpedAt:null});const s=e.lane==="todo"||e.waitsOn==="ready"||e.lane==="progress"&&e.waitsOn==="owner";if(!e.action||!s||e.stalled)return;const r=e.lane==="todo"&&rt.has(e.action.kind)?"todo":"review";Object.assign(e,{lane:r,waitsOn:e.waitsOn==="ready"?"ready":"owner",next:e.action.label})}d(Xn,"placeOwnerWait");const Et=d((e,t)=>e?e!=="todo":t.some(Q),"started"),$t="none set yet";function So(e,t){const n=t.filter(s=>s?.started);return e.next||et(e)||e.pushed||e.held||e.lane==="done"||st(n)?!1:!n.some(s=>!s.done)&&Et(e.lane,n)}d(So,"owesNext");const fe="visual",$e="helper",Yn=new Set(["grill","prototype",fe]),Z=d(e=>Yn.has(e.kind),"plans"),Q=d(e=>!Z(e),"builds"),zn={grill:"grilling",prototype:"prototyping"},Jn=d((e,t)=>`${e} ${t} by orchestrator`,"orchestratorDid"),At=d(e=>Jn(e,"stopped"),"stoppedWords"),Zn={grill:"grill-more",prototype:"prototype-request"},at=d((e,t)=>e.kind===Zn[t]&&!e.data?.question,"isRequest");function Bt(e,t,n,s){if(n.some(a=>a.kind===e&&a.started&&!a.done))return!0;const r=t.filter(a=>at(a,e)).pop();return!!(r&&!r.reply&&!s.some(a=>String(a)>String(r.ts))&&!n.some(a=>a.kind===e&&a.done&&String(a.started)>=String(r.ts)))}d(Bt,"planningOf");const vo=30*6e4;function xo(e,t){const n=[];for(const s of e.items){if(pe(s)||(s.agents||[]).some(a=>a.kind==="prototype"&&a.started&&!a.done))continue;const r=s.prototyping&&t.filter(a=>a.data?.key===s.key&&at(a,"prototype")).pop();r&&n.push({key:s.key,request:r.id,since:r.ts});for(const a of e.owner.filter(c=>c.topic===`item:${s.key}`&&c.state==="requested"))n.push({key:s.key,request:a.request.id,question:a.id,since:a.request.ts})}return n}d(xo,"prototypeWaits");function lt(e={},t=!1){return Object.entries(e).filter(([n,s])=>s&&typeof s=="object"&&(t||!n.startsWith("past-"))).map(([n,s])=>({name:n,kind:_t(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,log:s.session?.log||null,pid:s.session?.pid??null,calls:Array.isArray(s.calls)?s.calls:[],verified:s.verified||null,stopped:s.stopped||null,handed_back:s.handed_back||null,...s.pause?{pause:s.pause}:{},...s.kind===$e?{writes:!!s.writes,requested:s.requested||null}:{},...s.waits?{waits:s.waits}:{}})).sort((n,s)=>String(n.started||"").localeCompare(String(s.started||""))||n.name.localeCompare(s.name))}d(lt,"agentsOf");function it(e={}){return Object.values(e).filter(t=>t?.worktree?.path).sort((t,n)=>String(t.started||"").localeCompare(String(n.started||""))).at(-1)?.worktree.path??null}d(it,"worktreeOf");function es(e,t=[]){const n=e.filter(Q).map(s=>String(s.started||"")).filter(Boolean).sort().pop();return n===void 0?null:t.filter(s=>s.kind==="status"&&s.phase&&!s.planner&&String(s.ts)>=n).sort((s,r)=>String(s.ts).localeCompare(String(r.ts))).pop()?.phase||null}d(es,"reachedPhaseOf");function ts(e,t=[],n,s=null,r=null,a=!1){const c=e.filter(b=>b.started&&!b.done&&b.kind!==fe),l=c.every(Z),u=l?c:c.filter(Q),m=u.map(b=>String(b.started||"")).sort()[0];if(m===void 0)return null;const f=t.filter(b=>b.kind==="status"&&String(b.ts)>=m&&(l||!b.planner)).sort((b,i)=>String(b.ts).localeCompare(String(i.ts))).pop(),p=u.filter(b=>b.kind!==$e).at(-1),w=u.some(b=>b.kind===ue.install)?ue.install:p?_t(p.kind):"claude",v=l?null:u.find(b=>b.stopped&&String(b.stopped.at)>=String(b.started))?.stopped,_=f?.ts||m,x=v?v.at:s!==null&&s>Date.parse(_)?new Date(s).toISOString():_,P=f?f.phase||f.planner?f.text.replace(": "," \xB7 "):f.text||null:l?zn[w]:null,C=n-Date.parse(x),L=l?null:u.find(b=>b.pause)?.pause??null,T=!l&&!v&&(r==="held"||!!L?.paused),W=l||T?null:v?!1:r,h=T?!1:W===null?C>=mn:!W;return{progress:P,phase:f?.phase||null,at:_,kind:w,planner:l,silentSince:x,stale:h,alive:W,held:T,quiet:!!L||W===!0&&!a&&C>=wn,stopped:v?At(v.run):null,...L?{pause:{cause:L.cause,words:L.words,paused:!!L.paused}}:{}}}d(ts,"builderOf");const Oo={blocked:["nothing blocks it","nothing blocks it"],held:["you unhold it","unheld"],usage:["the usage pause lifts","the usage pause lifted"],bumped:["a slot frees","a slot freed"]},ns=15*6e4,ss=d(e=>(e.agents||[]).find(t=>t.name==="main"&&t.started&&!t.done&&Q(t))??null,"mainBuild"),os=d((e,t,n,{now:s,usageHeld:r})=>e.blocked||e.held||r(n??"claude")||!!(t.until&&s<Date.parse(t.until)),"stillPaused");function Eo(e,{now:t=Date.now(),usageHeld:n}){const s={ask:[],drop:[],stop:[],resume:[]},r=[];for(const a of e){if(a.kind!=="work"||a.lane!=="progress")continue;const c=ss(a);if(!c)continue;const{pause:l}=c;if(!l){a.builder?.phase!=="landing"&&n(c.kind??"claude")&&s.ask.push(a.key);continue}const u=os(a,l,c.kind,{now:t,usageHeld:n});if(l.paused){u||r.push({key:a.key,at:l.paused});continue}!u&&l.cause!=="bumped"?s.drop.push(a.key):t-Date.parse(l.at)>=ns&&s.stop.push(a.key)}return s.resume=r.sort((a,c)=>String(a.at).localeCompare(String(c.at))).map(a=>a.key),s}d(Eo,"pauseWork");const dt={opus:"Claude Opus",sonnet:"Claude Sonnet",codex:"Codex"},rs=d(({model:e,kind:t})=>dt[e]||dt[t]||(t==="claude"?dt.opus:null),"modelName"),_t=d(e=>e==="opus"||e==="sonnet"?"claude":e,"providerOf");function Ae(e){const t=rs(e),{kind:n}=e,s=e.label?.replace(/\bbuilder\b/,"implementer").replace(/\bBuilder\b/,"Implementer"),r=d(c=>c[0].toLowerCase()+c.slice(1),"lower");if(t&&Z(e))return`${t} ${n}`;if(t)return`${t} ${r(s?.replace(/^(?:claude\s+)?(?:opus|sonnet|codex)\b\s*/i,"")||"implementer")}`;const a=n?n[0].toUpperCase()+n.slice(1):"";return a?s?s.toLowerCase().includes(n.toLowerCase())?s:`${a} ${r(s)}`:a:s||"Implementer"}d(Ae,"agentName");const Rt=d((e,t)=>!!e.resolved&&(!e.resolved_at||String(e.resolved_at)>=String(t)),"resolvedSince");function as(e,t,n=[],s=[],r=[]){const a=t.filter(x=>x.data?.id===e.id),c=a[a.length-1]||null,l=r.filter(x=>x.data?.question===e.id).at(-1),u=d((x,...P)=>!!x&&P.every(C=>String(x.ts)>String(C||"")),"after"),m=!!(e.resolved&&e.resolved_at&&c&&String(c.ts)>e.resolved_at),f=n.filter(x=>x.data?.question===e.id&&u(x,c?.ts,l?.ts)).at(-1),p=u(l,c?.ts,f?.ts,e.resolved_at),w=e.resolved&&!m&&!p,v=w?s.filter(x=>x.data?.question===e.id&&String(x.ts)>String(e.resolved_at||"")).at(-1):null,_=p?"open":w?"resolved":f?"requested":c?c.reply?"replied":"answered":"open";return{...e,state:_,reopened:p,request:f?{id:f.id,ts:f.ts,note:f.data.note||"",choice:f.data.choice||null}:null,reask:v?{id:v.id,ts:v.ts}:null,answers:a.map(x=>({ts:x.ts,choice:x.data.choice||null,note:x.data.note||"",files:x.data.files||[],reply:x.reply,replies:x.replies,acted:Rt(e,x.ts)}))}}d(as,"ownerQuestion");const ge=d(e=>e.round??(De.test(e.url||"")?1:0),"roundOf");function ls(e,t){const n=d((s,r)=>s.confirm_of||(s.options||[]).some(a=>a.id===r.choice),"settles");return e.filter(s=>ge(s)===t&&s.answers.length).map(s=>[s,s.answers.at(-1)]).filter(([s,r])=>n(s,r)).map(([,s])=>s).sort((s,r)=>String(s.ts).localeCompare(String(r.ts))).at(-1)}d(ls,"roundSettledBy");function is(e){const t=e.find(r=>r.request&&!e.some(a=>ge(a)>ge(r)));if(t)return{n:ge(t)+1,state:"making"};const n=Math.max(0,...e.map(ge));if(!n)return null;const s=ls(e,n);return s?{n,state:"closed",take:s.choice}:e.some(r=>ge(r)===n&&r.state!=="resolved")?{n,state:"back"}:null}d(is,"roundState");function ds(e){const t=e.filter(n=>De.test(n.url||"")).map(n=>({n:ge(n),q:n.id,url:n.url,confirm:!!n.confirm_of,state:n.state})).sort((n,s)=>n.n-s.n);return t.length?{open:t.some(n=>n.state==="open"),rounds:t}:null}d(ds,"prototypesOf");const Be=d((e,t,n)=>{const s=e.get(t);s?s.push(n):e.set(t,[n])},"add"),_e=d((e,t)=>{const n=new Map;for(const s of e)Be(n,t(s),s);return n},"groupBy");function cs(e,t,n=new Set){const s=new Set(e.map(c=>c.id)),r=[...e];t.some(c=>!s.has(c.topic))&&r.push({id:"",title:"Other"});const a=_e(t,c=>s.has(c.topic)?c.topic:"");return r.map(c=>{const l=a.get(c.id)||[],u=d(p=>l.filter(w=>w.state===p).length,"count"),m=l.every(p=>p.state==="resolved")||n.has(c.id)&&!u("open"),f=l.length&&m?"resolved":"open";return{...c,state:f,questions:l.map(p=>p.id),open:u("open"),answered:u("answered"),replied:u("replied"),resolved:u("resolved"),round:is(l)}}).filter(c=>c.questions.length)}d(cs,"ownerTopics");const Xe={implement:"Implementation requested",prototype:"Prototype requested",grill:"Grill requested",visual:"Visual requested",reask:"Re-ask requested",hold:"Held",resume:"Resumed",done:"Marked done",staged:"Marked as staging",readied:"Marked as ready",related:"Related to",unrelated:"Unrelated from",followup:"Follow-up","followup-of":"Follow-up of"},Mt={"grill-more":"grill","prototype-request":"prototype","visual-request":"visual","reask-request":"reask"},It={"mark-ready":"Mark ready",...Object.fromEntries(Object.entries(Mt).map(([e,t])=>[e,Xe[t]]))},us=["grill-more","prototype-request","visual-request"],ps="answers submitted",qt={reply:"Reply",build:"Implement"},fs=d(e=>qt[e.data.intent],"intentLabel"),gs=d((e,t)=>{if(e.kind!=="prototype-request"||!t.has(e.data.question))return e.data.text||(e.data.files?.length?"":R(It,e.kind))||"";const n=t.get(e.data.question),s=(n.options||[]).find(r=>r.id===e.data.choice);return[Nt(t,n),s&&`${s.id}. ${s.label}`,e.data.note].filter(Boolean).join(`
2
2
  \u2192 `)},"actionText"),Ct=d((e,t)=>[...e.keys()].indexOf(t)+1,"numberOf"),Nt=d((e,t)=>`Question ${Ct(e,t.id)} \xB7 ${t.question}`,"numbered"),ks="question reopened",Ye=d(e=>e.data?.shared_by?.login?{by:e.data.shared_by.name||e.data.shared_by.login}:{},"byOf"),ms=d((e,t)=>{const n=t.get(e.card);return{...e,text:`${Xe[e.command]} ${n?`${n.ref}: ${n.title}`:e.card}`,link:`#selectedCard=${encodeURIComponent(e.card)}`}},"linkLine"),Dt=d(e=>({kind:"command",command:e,request:!0,mark:Xe[e]}),"commandLine"),ws=d((e,t)=>{if(e.kind==="item-message")return qt[e.data.intent]?{status:fs(e)}:{};const n={request:!0,command:Mt[e.kind]};return us.includes(e.kind)?(e.data.question?t.has(e.data.question):String(e.data.text||"").trim()||e.data.files?.length)?{status:It[e.kind]}:n:e.kind==="reask-request"?n:{}},"requestOf"),Lt=d(e=>[...e.filter(t=>t.who==="claude"&&!["status","agent","command",Ee,Ne.answer].includes(t.kind)).map(t=>t.ts),...e.filter(t=>t.kind==="ask").map(t=>t.reply?.at)].filter(Boolean).map(String).sort().pop()||"","claudeAtOf");function Tt(e,t={}){const n=new Map,s=new Map;for(const p of t.items||[])Be(n,p.topic,p),s.set(p.id,p.topic);const r=new Map,a=new Map,c=new Map,l=new Map,u=new Map,m=new Map,f=new Map;e.forEach((p,w)=>{f.set(p,w),p.data?.key&&kn.includes(p.kind)&&Be(r,p.data.key,p),p.kind==="owner-answer"&&s.has(p.data?.id)&&Be(a,s.get(p.data.id),p),p.kind===mt&&s.has(p.data?.question)&&Be(c,s.get(p.data.question),p),p.kind==="ask"&&l.set(p.id,p)});for(const p of e.filter(w=>w.kind==="ask-edited").sort((w,v)=>String(w.ts).localeCompare(String(v.ts)))){u.set(p.data?.id,p.data?.text);for(const w of p.data?.files||[])Be(m,p.data?.id,w)}return{byKey:r,answers:a,reopens:c,asks:l,edits:u,editFiles:m,questions:n,order:f}}d(Tt,"threadIndex");const ct=d((e,t)=>{const n=t.filter(s=>s?.length);return n.length>1?n.flat().sort((s,r)=>e.order.get(s)-e.order.get(r)):[...n[0]||[]]},"inLogOrder"),hs=/; its log is \S*\/\.claude\/kickflip\/logs\/([^/\s]+\.log)\.$/,ys=d(e=>{const t=!e.log&&typeof e.text=="string"?hs.exec(e.text):null;return t?{...e,text:`${e.text.slice(0,t.index)}.`,log:t[1]}:e},"withLog");function bs(e,t){const n=t.run?e.find(s=>s.started===t.run):e.filter(s=>Z(s)===!!t.planner&&s.started&&String(s.started)<=String(t.ts)&&(!s.done||String(t.ts)<=String(s.done))).pop();return n?{agent:Ae(n),run:ut(e,n),running:!n.done}:{}}d(bs,"agentOfLine");function ut(e,t){return t.label!==He?t.started:e.filter(n=>Q(n)&&n.label!==He&&n.started&&String(n.started)<String(t.started)).pop()?.started??t.started}d(ut,"sectionOf");function Ss(e,{lines:t=[],events:n=[],owner:s={},working:r={},fromAsk:a=null,links:c=[],agents:l={},moved:u=[],cards:m=new Map,index:f=Tt(n,s)}){const p=e.startsWith("ask:")?[e.slice(4)]:[a,...u].filter(Boolean),w=[...new Set([e,...p.map(i=>`ask:${i}`)])],v=new Map((f.questions.get(`item:${e}`)||[]).map(i=>[i.id,i])),_=f.answers.get(`item:${e}`)||[],x=new Set([...v.values()].map(i=>i.round_of).filter(Boolean)),P=lt(l,!0).filter(Q).map(i=>String(i.started||"")).filter(Boolean),C=new Set(_.filter(i=>Rt(v.get(i.data.id),i.ts)||x.has(i.data.id)||P.some(S=>S>String(i.ts))).map(i=>i.id)),L=lt(l,!0),T=[...L.flatMap(i=>[...i.started?[{ts:i.started,who:"claude",kind:"agent",text:`${Ae(i)} started`,agent:Ae(i),run:ut(L,i),finished:!1,eta:i.done?null:i.eta,...i.log?{log:i.log}:{},...Z(i)?{planner:!0}:{}}]:[],...i.done?[{ts:i.done,who:"claude",kind:"agent",text:`${Ae(i)} finished`,agent:Ae(i),run:ut(L,i),finished:!0,eta:null,...Z(i)?{planner:!0}:{}}]:[],...i.calls.length?[{ts:i.done||i.calls.at(-1).at,who:"claude",kind:"calls",text:Ae(i),calls:i.calls.map(S=>S.text),verified:i.verified}]:[]]),...t.map(ys).map(i=>i.card?ms(i,m):i).map(i=>({ts:i.ts,who:i.who||"claude",...i.log?{log:i.log}:{},...i.kind==="status"?{kind:"status",...i.planner?{planner:!0}:{},...bs(L,i)}:{},...i.kind===fe||i.kind===Ee?{kind:i.kind}:{},...i.kind===Qe?{kind:Qe,line:i.line,...gt(i)}:{},...i.command?Dt(i.command):{},...i.card?{card:i.card}:{},...Object.values(Ne).includes(i.kind)?{kind:i.kind,ask:i.ask}:{},text:i.text||"",html:i.html||null,link:i.link||null,via:i.via||null,...i.by?{by:i.by}:{}})),...ct(f,w.map(i=>f.byKey.get(i))).map(i=>({ts:i.ts,who:"love",via:"site",kind:i.kind,text:gs(i,v),files:i.data.files||[],eventId:i.id,reply:i.reply,replies:i.replies,...ws(i,v),...Ye(i)})),..._.map(i=>{const S=[i.data.label||i.data.choice,i.data.note].filter(Boolean).join(" \xB7 ");return{ts:i.ts,who:"love",via:"questions",kind:i.kind,text:`${Nt(v,v.get(i.data.id))}
3
3
  \u2192 ${S}`,files:i.data.files||[],eventId:i.id,reply:i.reply,replies:i.replies,status:ps,...Ye(i)}}),...(f.reopens.get(`item:${e}`)||[]).map(i=>({ts:i.ts,who:"love",kind:i.kind,request:!0,mark:ks,text:`Question ${Ct(v,i.data.question)} was reopened`,...Ye(i)})),...ct(f,[...new Set(p)].map(i=>f.asks.has(i)?[f.asks.get(i)]:[])).flatMap(i=>[{ts:i.ts,who:"love",via:"ask",kind:i.kind,text:i.data?.text||"",files:i.data?.files||[],eventId:i.id,reply:i.reply,replies:i.replies,...u.includes(i.id)?{moved:!0}:{},...Ye(i),...typeof i.data?.from?.title=="string"?{from:i.data.from.title}:{}},...i.data?.quick===!0?[{ts:i.ts,who:"love",...Dt("implement"),text:Xe.implement}]:[],...c.map(S=>({ts:i.ts,who:"claude",kind:"ask-link",text:S.folded?`Moved to ${S.ref}: ${S.title}`:`${S.became?"Became":"Added to"} ${S.ref}`,link:`#selectedCard=${encodeURIComponent(S.key)}`,html:null,via:null}))])].sort((i,S)=>String(i.ts).localeCompare(String(S.ts))),W=Lt(T),{edits:h,editFiles:b}=f;for(const i of T){if(!i.eventId)continue;(i.kind==="ask"||i.kind==="item-message")&&typeof h.get(i.eventId)=="string"&&Object.assign(i,{text:h.get(i.eventId),edited:!0,files:[...i.files,...b.get(i.eventId)||[]]});const S=i.kind==="ask"&&(c.length>0||!!i.moved);i.state=i.reply?"replied":S||C.has(i.eventId)||W>String(i.ts)?"answered":R(r,i.eventId)?"working":"waiting",i.canDelete=i.kind==="item-message"&&(i.state==="working"||i.state==="waiting"),i.canEdit=i.canDelete}return vs(T)}d(Ss,"threadOf");const Pt=["waiting","working","answered","replied"];function vs(e){const t=new Map;for(const n of e)n.kind==="owner-answer"&&t.set(n.ts,[...t.get(n.ts)||[],n]);return e.flatMap(n=>{if(n.kind!=="owner-answer")return[n];const s=t.get(n.ts);return s.length===1?[n]:s[0]!==n?[]:[{...n,text:s.map(r=>r.text).join(`
4
4
 
@@ -8,7 +8,7 @@
8
8
  // - from 900px, #waiting, one line each, in the pane beside the lanes, over Waiting on you mounted there
9
9
  // (view-questions.js, embedded) while the width lasts. Waiting on you shows the questions there, so #waiting leaves out
10
10
  // a line that only asks questions, and with nothing else left the orange section is gone. Every questions URL lands
11
- // here at that width (shell.js go, focusQuestion), and two taps on a pill focus its card on the board
11
+ // here at that width (shell.js go, focusQuestion), and a click on a pill focus its card on the board
12
12
  // (docs/adr/2026-11-07-1300-board-and-open-questions-are-two-pages.md). A prototype opens in the pane too, in place of
13
13
  // Waiting on you until its ‹ Open questions (openViewer).
14
14
  // Below 900px the header's ? opens Waiting on you, with the open-question count as its badge.
@@ -156,7 +156,7 @@ BoardView.part((B) => {
156
156
 
157
157
  // From 900px, Waiting on you in the pane under the Needs you lines, mounted while the width lasts.
158
158
  let questions = null;
159
- // Two taps on a pane pill focus its topic on the board: item:<key> is a card (an ask is one, ask:<id>),
159
+ // A click on a pane pill, or on the topic's title, focuses its topic on the board: item:<key> is a card (an ask is one, ask:<id>),
160
160
  // wave:<slug> or file:<slug> a band; any other topic, or one the board doesn't show, does nothing.
161
161
  function openOnBoard(id) {
162
162
  if (id.startsWith('item:')) B.focusOnBoard({ key: id.slice(5) });
@@ -671,6 +671,8 @@ main > * { transition: opacity .25s ease; }
671
671
  @media (prefers-reduced-motion: no-preference) { .view-questions .topics button.arrived { animation: pill-arrive .6s ease-out; } }
672
672
  .view-questions .topics .n { color: var(--muted); margin-left: 4px; }
673
673
  .view-questions .topichead { margin: 2px 0 6px; }
674
+ .view-questions .topichead .topic-title { background: none; border: 0; padding: 0; color: var(--text, inherit); font: inherit; font-size: 18px; font-weight: 600; text-align: left; cursor: pointer; }
675
+ .view-questions .topichead .topic-title:hover { text-decoration: underline; }
674
676
  .view-questions details.done > summary { cursor: pointer; color: var(--muted); font-size: 13px; margin: 16px 0 4px; }
675
677
  .view-questions details.resolved-group > summary { list-style: none; min-height: 44px; display: flex; align-items: center; gap: 8px; font-size: 14px; margin: 12px 0; }
676
678
  .view-questions details.resolved-group > summary::-webkit-details-marker { display: none; }
@@ -851,9 +853,10 @@ button[aria-busy="true"]:is([data-card-bump]) svg { fill: currentColor; }
851
853
  .external-links .install.tone-ready { color: var(--color-success); }
852
854
  .external-links .install.tone-danger { color: var(--color-danger); }
853
855
  .actions button[aria-disabled="true"] { opacity: .5; cursor: default; }
854
- .tb-main { margin-top: 14px; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; padding: 9px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); font-size: 13px; }
855
- .tb-main > svg { flex: none; color: var(--muted); }
856
- .tb-main-text { flex: 1 1 200px; min-width: 0; overflow-wrap: anywhere; }
856
+ .tb-main { margin-top: 14px; display: flex; flex-direction: column; gap: 4px; padding: 9px 12px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); font-size: 13px; }
857
+ .tb-main-row { display: flex; align-items: center; gap: 6px 10px; min-height: 30px; }
858
+ .tb-main-row > svg { flex: none; color: var(--muted); }
859
+ .tb-main-text { flex: 1 1 0; min-width: 0; overflow-wrap: anywhere; }
857
860
  .tb-main .meta { justify-content: flex-end; margin-left: auto; }
858
861
  /* #60: Main's refresh icon right of its timestamp; a failed pull's reason shows on hover, or on a tap on touch */
859
862
  .tb-main .main-pull { position: relative; display: inline-flex; align-items: center; vertical-align: middle; margin-left: 6px; }
@@ -147,18 +147,20 @@
147
147
  const why = failed ? `<span class="main-pull-why${open ? ' open' : ''}" role="tooltip">${esc(pull.error)}</span>` : '';
148
148
  return `<span class="main-pull"><button class="ghost gh-refresh${busy ? ' spinning' : ''}${failed ? ' failed' : ''}" type="button" data-main-pull aria-label="${esc(label)}"${busy ? ' aria-disabled="true"' : ''}>${BoardUI.refreshIcon(14)}</button>${why}</span>`;
149
149
  }
150
- // While Main re-serves, or after its new build failed, the old server still serves (main.serving): the bar names its
151
- // commit and keeps Visit to it, with Serving… or Failed left of Visit.
150
+ // Main's bar is two rows, each control in its own steady place: the name and Visit, then the commit, its age, the
151
+ // refresh icon and the state. While Main re-serves, or after its new build failed, the old server still serves
152
+ // (main.serving): the bar names its commit and keeps Visit to it, with Updating… or Update failed on row 2.
152
153
  function mainBar(main, pulling) {
153
154
  if (!main) return '';
154
155
  const words = tryWords(main.kind);
155
156
  const url = tryURL(main);
156
157
  const old = main.serving;
157
- const status = main.state === 'running' ? badge('notice', spinner + esc(old ? 'Serving…' : words.going), { attrs: { role: 'status' } })
158
- : main.state === 'failed' ? badge('danger', esc(old ? 'Failed' : words.failed), { button: true, attrs: { type: 'button', 'data-main-log': '', 'aria-haspopup': 'dialog' }, title: 'Read main’s build log and retry this tip.' }) : '';
158
+ const status = main.state === 'running' ? badge('notice', `${spinner}Updating…`, { attrs: { role: 'status' } })
159
+ : main.state === 'failed' ? badge('danger', 'Update failed', { button: true, attrs: { type: 'button', 'data-main-log': '', 'aria-haspopup': 'dialog' }, title: 'Read main’s build log and retry this tip.' }) : '';
159
160
  const visit = (main.state === 'ready' || old) && url ? badge('ready', `${words.done} ${LINK_ARROW}`, { href: url, attrs: main.kind === 'web' ? EXTERNAL : {} }) : '';
160
161
  const shown = old || main;
161
- return `<div class="tb-main" role="region" aria-label="Main’s version">${BRANCH}<div class="tb-main-text">${esc(main.slug)} · <strong>${esc(main.branch)}</strong>@<code>${esc(shown.sha)}</code> · ${esc(ago(shown.at))}${mainPull(main.pull, pulling)}</div><div class="meta">${status}${visit}</div></div>`;
162
+ return `<div class="tb-main" role="region" aria-label="Main’s version"><div class="tb-main-row">${BRANCH}<div class="tb-main-text">${esc(main.slug)}</div><div class="meta">${visit}</div></div>`
163
+ + `<div class="tb-main-row"><div class="tb-main-text"><strong>${esc(main.branch)}</strong>@<code>${esc(shown.sha)}</code> <span class="muted">${esc(lasted(shown.at))}</span>${mainPull(main.pull, pulling)}</div><div class="meta">${status}</div></div></div>`;
162
164
  }
163
165
  // The open card's Merge, while the board's auto-merge is off: it confirms, then sends card-merge, waiting while the
164
166
  // server records it (app-cards.js), and waits (ui.js BUSY) while the session merges it, or the board server lands a
@@ -86,7 +86,7 @@
86
86
  }
87
87
  // An arrival (arrive) slides in with a glow and settles (style.css .arrived).
88
88
  const pillHtml = (t, arrived = false) => `<button class="${arrived ? 'arrived ' : ''}${t.progress ? 'progress' : ''}" data-topic="${esc(t.id)}" aria-pressed="${t.id === topicId}" title="${esc(t.title)}"><span class="name">${esc(topicName(t))}</span><span class="n">${t.state === 'resolved' ? 'resolved' : t.open}</span></button>`;
89
- const topicHead = (t) => t ? `<div class="topichead muted small"><span>${esc(t.title)}${t.summary ? ` · ${esc(t.summary)}` : ''}</span>${roundChip(t.round)}</div>` : '';
89
+ const topicHead = (t) => t ? `<div class="topichead"><button class="topic-title" data-open-card="${esc(t.id)}">${esc(t.title)}</button>${t.summary ? `<span class="muted small">${esc(t.summary)}</span>` : ''}${roundChip(t.round)}</div>` : '';
90
90
 
91
91
  // Only open questions changed here since their last answer, with files attached, reopened with a pick, or with a
92
92
  // suggested option pre-checked and never answered, are sent; a sent one is read-only.
@@ -368,7 +368,6 @@
368
368
  scroll(0);
369
369
  }
370
370
 
371
- let lastTap = { id: null, at: 0 };
372
371
  // item:<key> is a card, wave:<slug> or file:<slug> a band. Embedded, the board is beside the pane; alone, it is the page before this one.
373
372
  function openOnBoard(id) {
374
373
  if (ctx.embedded) return ctx.openOnBoard(id);
@@ -382,17 +381,17 @@
382
381
  if (eye) { askPrototype(byId(eye.closest('section.q').dataset.q), eye); return; }
383
382
  const reopenBadge = ev.target.closest && ev.target.closest('[data-reopen]');
384
383
  if (reopenBadge) { if (!reopenBadge.disabled) reopen(byId(reopenBadge.dataset.reopen), reopenBadge); return; }
384
+ const title = ev.target.closest && ev.target.closest('[data-open-card]');
385
+ if (title) { openOnBoard(title.dataset.openCard); return; }
385
386
  const b = ev.target.closest && ev.target.closest('[data-topic]');
386
387
  if (b) {
387
- // Two taps on one pill within 300ms open its card on the board; a touch screen sends no dblclick.
388
- const now = Date.now();
389
- if (lastTap.id === b.dataset.topic && now - lastTap.at < 300) { lastTap = { id: null, at: 0 }; openOnBoard(b.dataset.topic); return; }
390
- lastTap = { id: b.dataset.topic, at: now };
388
+ // Embedded in the desktop pane, one click also focuses the topic's card on the board; a phone only picks the topic.
391
389
  soloId = null; setTopic(b.dataset.topic); sentLine = ''; shown = '';
392
390
  const first = topics.find((t) => t.id === topicId)?.questions[0];
393
391
  if (first) setHash(first);
394
392
  ctx.onTopic?.(topicId);
395
393
  render(); scroll(0);
394
+ if (ctx.embedded) openOnBoard(topicId);
396
395
  }
397
396
  });
398
397
  B.on(root, 'change', (ev) => {
package/hub/readiness.mjs CHANGED
@@ -1 +1 @@
1
- var x=Object.defineProperty;var s=(t,i)=>x(t,"name",{value:i,configurable:!0});import b from"node:os";import{execFile as $}from"node:child_process";import{githubLogin as C}from"./pr-report.mjs";import{clock as S}from"./schedule.mjs";import{gateFile as T,limitedUntil as P}from"../shared/gh.mjs";import{codexStatusReader as y}from"../shared/codex.mjs";import{linearStatus as R}from"./linear.mjs";import{slackStatus as B}from"./slack.mjs";import{readSettings as N,subscriptions as I}from"./push.mjs";import{tailscaleState as O}from"../bin/install.mjs";import{tailscaleBin as D,tailscaleInstallCommand as H}from"../shared/tailscale.mjs";const h=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack","Reviewer DMs and digests wait on this."],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],L=s((t,i,a)=>({settings:t,label:`Hub settings \u203A ${i}`,text:a}),"settingsFix");function k(t,{now:i=Date.now(),platform:a=process.platform}={}){const l=s((o,e)=>{switch(o){case"github":return e?.limited?{ready:!1,limited:!0,missing:`resets at ${S(new Date(e.limited),new Date(i))}`}:e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",keyButton:"Change key"}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`,keyButton:"Change key"}:{ready:!1,missing:"No Linear key set",keyButton:"Add key"};case"slack":return e.set?e.refused?{ready:!1,missing:"Slack refused the token",keyButton:"Change key"}:{ready:!0,detail:`Token set \xB7 ${e.team} workspace, sending as ${e.user}`,keyButton:"Change key"}:{ready:!1,missing:"No Slack token set",keyButton:"Add key"};case"tailscale":{let n,d;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(n=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,d="sudo tailscale set --operator=$USER"):(n="Tailscale / is not serving the hub",d="kickflip doctor --fix"):(n="Tailscale isn't signed in",d="tailscale up"):(n="Tailscale isn't installed on the host",d=H(a)),n?{ready:!1,missing:n,fix:{command:d}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices.length?{ready:!0,detail:`Pushes on \xB7 ${e.devices.length} device${e.devices.length===1?"":"s"}`,devices:e.devices.map(({id:n,kind:d,browser:f,added:m})=>({id:n,kind:d,browser:f,added:m}))}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:L("notifications","Notifications","Turn on notifications in")}}},"row");return h.filter(([o])=>Object.hasOwn(t,o)).map(([o,e,n])=>({id:o,name:e,...l(o,t[o]),waits:n}))}s(k,"readinessRows");const A=s(t=>(i,a)=>new Promise(l=>{$(i,a,{env:t,timeout:3e3},(o,e)=>l({status:o?1:0,stdout:e}))}),"asyncRun");function F({home:t=b.homedir(),env:i=process.env,now:a=Date.now,cacheMs:l=6e4,probes:o={}}={}){const e=y({home:t,env:i,now:a,cacheMs:l}),n={github:s(()=>C({env:i}).catch(()=>null),"github"),codex:e,linear:s(()=>R(t),"linear"),slack:s(()=>B(t),"slack"),tailscale:s(()=>O({run:A(i),port:Number(i.KICKFLIP_HUB_PORT||4500),tailscale:D({env:i})}),"tailscale"),notifications:s(()=>({on:N(t).notifications.on,devices:I(t)}),"notifications"),...o},d=new Map;async function f(r,c){if(!["github","tailscale"].includes(r))return n[r]({fresh:c});const p=r==="github"&&P({file:T(t),now:a()});if(p)return{limited:p};const g=d.get(r);if(!c&&g&&a()-g.at<l)return g.value;const u=Promise.resolve().then(()=>n[r]());return d.set(r,{at:a(),value:u}),u}s(f,"check");const m=s(async({fresh:r=!1}={})=>k(Object.fromEntries(await Promise.all(h.map(async([c])=>[c,await f(c,r)]))),{now:a()}),"rows");async function w(r){const[c]=k({[r]:await f(r,!1)},{now:a()});return c.ready||!c.waits?null:c.waits.replace("this",`${c.name}: ${c.missing.replace(/\.$/,"")}`)}return s(w,"waiting"),{rows:m,waiting:w}}s(F,"readinessReader");const E=y(),X=F({probes:{codex:E}});export{E as readCodex,X as readiness,F as readinessReader,k as readinessRows};
1
+ var x=Object.defineProperty;var s=(t,i)=>x(t,"name",{value:i,configurable:!0});import b from"node:os";import{execFile as $}from"node:child_process";import{githubLogin as C}from"./pr-report.mjs";import{clock as S}from"./schedule.mjs";import{gateFile as T,limitedUntil as P}from"../shared/gh.mjs";import{codexStatusReader as y}from"../shared/codex.mjs";import{linearStatus as R}from"./linear.mjs";import{slackStatus as B}from"./slack.mjs";import{readSettings as N,subscriptions as I}from"./push.mjs";import{tailscaleState as O}from"../bin/install.mjs";import{tailscaleBin as A,tailscaleInstallCommand as D}from"../shared/tailscale.mjs";const H={slack:"Reviewer DMs and digests wait on this."},h=[["github","GitHub","GitHub polling and PR reports wait on this."],["codex","Codex","Codex implementing and reviewing wait on this; Codex share stays Off."],["linear","Linear",null],["slack","Slack",null],["tailscale","Tailscale","Try builds wait on this."],["notifications","Notifications","Pushes wait on this."]],L=s((t,i,a)=>({settings:t,label:`Hub settings \u203A ${i}`,text:a}),"settingsFix");function k(t,{now:i=Date.now(),platform:a=process.platform}={}){const d=s((r,e)=>{switch(r){case"github":return e?.limited?{ready:!1,limited:!0,missing:`resets at ${S(new Date(e.limited),new Date(i))}`}:e?{ready:!0,detail:`gh logged in as ${e}`}:{ready:!1,missing:"gh isn't logged in on the host",fix:{command:"gh auth login"}};case"codex":return e.usable?{ready:!0,detail:"Installed and logged in"}:{ready:!1,missing:e.words,fix:{command:e.fix}};case"linear":return e.set?e.refused?{ready:!1,missing:"Linear refused the key",keyButton:"Change key"}:{ready:!0,detail:`Key set \xB7 ${e.workspace} workspace`,keyButton:"Change key"}:{ready:!1,missing:"No Linear key set",keyButton:"Add key"};case"slack":return e.set?e.refused?{ready:!1,missing:"Slack refused the token",keyButton:"Change key"}:{ready:!0,detail:`Token set \xB7 ${e.team} workspace, sending as ${e.user}`,keyButton:"Change key"}:{ready:!1,missing:"No Slack token set",keyButton:"Add key"};case"tailscale":{let n,c;return e.installed?e.signedIn?e.pointsAtHub?e.operatorChecked&&e.operator!==e.username&&(n=`Tailscale operator is ${e.operator||"none"} (expected ${e.username})`,c="sudo tailscale set --operator=$USER"):(n="Tailscale / is not serving the hub",c="kickflip doctor --fix"):(n="Tailscale isn't signed in",c="tailscale up"):(n="Tailscale isn't installed on the host",c=D(a)),n?{ready:!1,missing:n,fix:{command:c}}:{ready:!0,detail:`Serving ${e.dns}${e.operatorChecked?" \xB7 operator "+e.operator:""}`}}case"notifications":return e.on&&e.devices.length?{ready:!0,detail:`Pushes on \xB7 ${e.devices.length} device${e.devices.length===1?"":"s"}`,devices:e.devices.map(({id:n,kind:c,browser:m,added:g})=>({id:n,kind:c,browser:m,added:g}))}:{ready:!1,missing:e.on?"No device has turned on notifications":"Pushes are off",fix:L("notifications","Notifications","Turn on notifications in")}}},"row");return h.filter(([r])=>Object.hasOwn(t,r)).map(([r,e,n])=>({id:r,name:e,...d(r,t[r]),waits:n}))}s(k,"readinessRows");const E=s(t=>(i,a)=>new Promise(d=>{$(i,a,{env:t,timeout:3e3},(r,e)=>d({status:r?1:0,stdout:e}))}),"asyncRun");function F({home:t=b.homedir(),env:i=process.env,now:a=Date.now,cacheMs:d=6e4,probes:r={}}={}){const e=y({home:t,env:i,now:a,cacheMs:d}),n={github:s(()=>C({env:i}).catch(()=>null),"github"),codex:e,linear:s(()=>R(t),"linear"),slack:s(()=>B(t),"slack"),tailscale:s(()=>O({run:E(i),port:Number(i.KICKFLIP_HUB_PORT||4500),tailscale:A({env:i})}),"tailscale"),notifications:s(()=>({on:N(t).notifications.on,devices:I(t)}),"notifications"),...r},c=new Map;async function m(o,l){if(!["github","tailscale"].includes(o))return n[o]({fresh:l});const f=o==="github"&&P({file:T(t),now:a()});if(f)return{limited:f};const p=c.get(o);if(!l&&p&&a()-p.at<d)return p.value;const u=Promise.resolve().then(()=>n[o]());return c.set(o,{at:a(),value:u}),u}s(m,"check");const g=s(async({fresh:o=!1}={})=>k(Object.fromEntries(await Promise.all(h.map(async([l])=>[l,await m(l,o)]))),{now:a()}),"rows");async function w(o){const[l]=k({[o]:await m(o,!1)},{now:a()}),f=l.waits??H[o];return l.ready||!f?null:f.replace("this",`${l.name}: ${l.missing.replace(/\.$/,"")}`)}return s(w,"waiting"),{rows:g,waiting:w}}s(F,"readinessReader");const G=y(),Y=F({probes:{codex:G}});export{G as readCodex,Y as readiness,F as readinessReader,k as readinessRows};
package/hub/try.mjs CHANGED
@@ -1,9 +1,12 @@
1
- var we=Object.defineProperty;var a=(o,d)=>we(o,"name",{value:d,configurable:!0});import j from"node:fs";import O from"node:path";import Gt from"node:os";import Ot from"node:http";import{execFile as ye,spawn as ge}from"node:child_process";import{createInterface as Lt}from"node:readline";import{promisify as $e}from"node:util";import{listBoards as z,readManifest as V,readJSON as lt,saveJSON as Ut,appendEvent as ve,GITHUB_STATE as be,REPO_FETCH as Kt}from"./boards.mjs";import{boardRepo as et,githubFromOrigin as Jt,mainRefOf as ke}from"./board-repo.mjs";import{gitReachFault as Et,remoteFromInput as Se,remoteGitEnv as Oe}from"./repos.mjs";import{readiness as Ee}from"./readiness.mjs";import{tailscaleBin as Te}from"../shared/tailscale.mjs";import{buildScriptError as xe,buildScriptOf as zt}from"../board/build-script.mjs";import{appendStatus as Yt,recordedWorktree as je,servedRoundsOpen as _e,setItem as Ie}from"../board/store.mjs";import{BASE_KEY as L,baseWorktreeOf as Ae}from"../board/prototype-base.mjs";import{cardPRs as Me,hasPR as Pe,prKeyOf as Ce}from"../shared/card-prs.mjs";import{hubGate as Ne}from"../board/on-host.mjs";import{pipeUpgrade as De,refuse as Fe,refuseUpgrade as Re}from"./front-door.mjs";import{shareOf as Be}from"./share.mjs";class _ extends Error{static{a(this,"BuildError")}constructor(d,y=400){super(d),this.status=y}}const dt=$e(ye),I=a(async(o,...d)=>(await dt("git",["-C",o,...d],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),He=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,q={first:8500,last:8599},Tt=a(o=>/^\d+$/.test(String(o))&&Number(o)>=q.first&&Number(o)<=q.last,"isTryPort"),We=30*6e4,Qt=["tailscale-user-login","tailscale-user-name","tailscale-user-profile-pic","x-forwarded-for"],Ge=["x-frame-options"],Le=a(o=>{const d=Object.fromEntries(Object.entries(o).filter(([y])=>!Ge.includes(y)));if(typeof d["content-security-policy"]=="string"){const y=d["content-security-policy"].split(";").filter(l=>!/^\s*frame-ancestors\b/i.test(l)).join(";").trim();y?d["content-security-policy"]=y:delete d["content-security-policy"]}return d},"withoutFraming"),ht={main:"main",[L]:"prototypes"},R=a(o=>!Object.hasOwn(ht,o),"isCard"),Vt=a((o,d)=>O.join(o,zt(d)),"scriptAt"),mt=a(o=>j.existsSync(o),"exists"),qt=a((o,d)=>O.join(o.dir,"data","try",`${d.split("/").at(-1)}.log`),"logFileOf");function xt(o,d,y){const l=qt(o,d);return j.mkdirSync(O.dirname(l),{recursive:!0}),j.writeFileSync(l,y.message+`
2
- `,{mode:384}),j.chmodSync(l,384),l}a(xt,"failureLog");const Xt=a(o=>new Promise(d=>{const y=Ot.get({host:"127.0.0.1",port:o,path:"/",timeout:1e3},l=>{l.destroy(),d(!0)});y.on("timeout",()=>y.destroy()),y.on("error",()=>d(!1))}),"probe"),Zt=a((o,d)=>{try{process.kill(-o,d)}catch{}},"signalGroup"),te=a(o=>{try{return process.kill(-o,0),!0}catch{return!1}},"groupUp"),ee=a(o=>new Promise(d=>{const y=a(()=>{clearTimeout(l),clearInterval(g),d()},"finish");Zt(o,"SIGTERM");let l=setTimeout(()=>{Zt(o,"SIGKILL"),l=setTimeout(y,1e3)},4e3);const g=setInterval(()=>{te(o)||y()},20)}),"endGroup");async function Ue(o){const{stdout:d}=await dt("ps",["-axo","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3}),y=d.split(`
3
- `).map(l=>/^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(l)).filter(l=>l&&Number(l[2])===o).map(l=>({pid:l[1],command:l[3],cwd:""}));if(!y.length)return y;try{const{stdout:l}=await dt("lsof",["-a","-d","cwd","-Fpn","-p",y.map(f=>f.pid).join(",")],{encoding:"utf8",timeout:5e3});let g;for(const f of l.split(`
4
- `))f[0]==="p"?g=y.find(k=>k.pid===f.slice(1)):f[0]==="n"&&g&&(g.cwd=f.slice(1))}catch{}return y}a(Ue,"groupMembers");const Ke=a((o,d)=>o===d||o.startsWith(d+O.sep),"insideDir"),Je=a((o,d)=>Ke(o.cwd,d)||o.command.includes(d+O.sep)||o.command.endsWith(d),"inWorktree");async function re(o,d,y){const l=await je(o.dir,d),[g,f]=d.split("/"),k=o.files.find(S=>S.slug===g)?.itemFile,$=l?.path||k?.items.find(S=>S.id===f)?.worktree;if(typeof $!="string"||!$||$.split(O.sep).includes(".."))return"";let m=l?.repo;const p=(o.github||k?.repo||"").toLowerCase();if(!m&&p){const S=[o.owner?.repo,o.dir,...Object.values(y?.sites||{}).map(E=>E.owner?.repo)];for(const E of new Set(S.filter(M=>typeof M=="string"&&O.isAbsolute(M))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await I(E,"remote","get-url","origin"))?.[1].toLowerCase()===p){m=await I(E,"rev-parse","--show-toplevel");break}}catch{}}if(!l?.path&&!m||l?.path&&!O.isAbsolute($))return"";try{const S=[O.join(j.realpathSync(Gt.homedir()),".claude","kickflip","worktrees")];if(typeof m=="string"&&O.isAbsolute(m))try{S.push(O.join(j.realpathSync(m),".claude","worktrees"))}catch{}const E=O.resolve(m||"/",$),M=j.realpathSync(E);return S.some(U=>E.startsWith(U+O.sep)&&M.startsWith(U+O.sep))&&j.statSync(M).isDirectory()?M:""}catch{return""}}a(re,"installWorktree");function ze({now:o=Date.now,answers:d=Xt,lingers:y=Xt,log:l=console.log,waiting:g=a(async()=>null,"waiting"),event:f=ve,gate:k=Ne}={}){const $=new Map,m=new Map,p=new Map,S=a(()=>[...m.values(),...p.values()],"live"),E=new Set,M=new Set,U=new Map,rt=new Map,gt=new Map,it=new Map;let H=!1,W=null,Z=null,ut=null;const ft=a(()=>new Date(o()).toISOString(),"stamp");function C(t){if($.has(t.dir))return $.get(t.dir);const e=O.join(t.dir,"data","try.json"),r=lt(e,{kind:null,main:null,cards:{}}),s={...Array.isArray(r.groups)&&r.groups.length?{groups:r.groups}:{},...Tt(r.main_port)?{main_port:r.main_port}:{},kind:r.kind||null,main:r.main||null,cards:{...r.cards},...r.pull?{pull:{...r.pull,running:void 0}}:{}},i={state:s,file:e,written:JSON.stringify(r)};$.set(t.dir,i),s.main_port&&rt.set(t.dir,s.main_port);const n=Object.entries(s.cards).filter(([,c])=>c.state==="running");for(const[c]of n)M.add(D(t,c));return s.kind==="web"?(s.cards=Object.fromEntries(n),s.main&&s.main.state!=="failed"&&(delete s.main.state,delete s.main.port,delete s.main.https)):s.main?.state==="running"&&(delete s.main.state,delete s.main.sha),s.main&&delete s.main.serving,r.prototypes?.state==="failed"&&(s.prototypes=r.prototypes),A(t),i}a(C,"stateOf");function A(t){const e=$.get(t.dir),r=JSON.stringify(e.state);r!==e.written&&(Ut(e.file,e.state),e.written=r)}a(A,"persist");function ie(t){const{state:e}=C(t.board);t.group={pgid:t.child.pid,key:t.key,worktree:t.worktree},(e.groups||=[]).push(t.group),A(t.board)}a(ie,"recordGroup");function se(t){const{state:e}=C(t.board);e.groups?.includes(t.group)&&(e.groups=e.groups.filter(r=>r!==t.group),e.groups.length||delete e.groups,A(t.board))}a(se,"forgetGroup");async function ne(t){const{state:e}=C(t),r=e.groups||[];delete e.groups,A(t);for(const s of r){if(!te(s.pgid))continue;let i=[];try{i=await Ue(s.pgid)}catch(n){l(`try startup sweep: could not read group ${s.pgid}: ${n.message}`)}i.some(n=>Je(n,s.worktree))&&(await ee(s.pgid),l(`stopped leftover try /${t.route} ${s.key} (pgid ${s.pgid})`),await jt(t,s.key,s.port))}}a(ne,"sweepGroups");async function jt(t,e,r){if(!r||!await y(r))return;const s=`Port ${r} still answers after the run was stopped.`;R(e)&&await Yt(t.dir,e,"run-failed",{kind:"Serve",code:1,why:s}).catch(i=>l(`build /${t.route} ${e}: ${i.message}`)),l(`build /${t.route} ${e}: ${s}`)}a(jt,"checkQuiet");const D=a((t,e)=>`${t.dir}\0${e}`,"runKey"),_t=a((t,e)=>R(e)?t.cards[e]:t[ht[e]],"entryOf"),oe=a((t,e,r)=>{R(e)?t.cards[e]=r:t[ht[e]]=r},"setEntry"),It=a((t,e)=>{R(e)?delete t.cards[e]:t[ht[e]]=null},"dropEntry");function K(t,e,r){const{state:s}=C(t),i=_t(s,e);if(!i)return;const n=Object.hasOwn(r,"state")&&r.state!==i.state;Object.assign(i,r,R(e)&&n?{at:ft()}:{}),A(t)}a(K,"update");function $t(t){const e=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(r=>r.site)];for(const r of e){if(!r?.dir)continue;const s=lt(O.join(r.dir,"data","try.json"),{}).main_port;Tt(s)&&rt.set(r.dir,Number(s))}}a($t,"reserveMains");function ae(t){const{board:e,key:r}=t,s=D(e,r),i=r==="main"?rt.get(e.dir):U.get(s);if(i)return i;const n=new Set([...rt.values(),...U.values()]);let c=q.first;for(;n.has(c)&&c<=q.last;)c++;if(c>q.last)throw new Error(`No try ports available (${q.first}\u2013${q.last}). Stop a served card and retry.`);return r==="main"?(rt.set(e.dir,c),C(e).state.main_port=c,A(e)):U.set(s,c),c}a(ae,"publicPort");const st=a(async t=>{const e=Te();if(!e)throw new Error("Tailscale isn't installed on this host");return dt(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function At(t){try{await st(["serve",`--https=${t}`,"off"])}catch(e){l(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}a(At,"removePort");const Mt=a(t=>Be(lt(O.join(t.dir,"board.json"),{})),"sharedWith");async function Pt(t,e){const r=await k(t),s=t.headers["tailscale-user-login"];return r!=="other"||!s||!Mt(e).includes(s)?r:t.method==="GET"||t.method==="HEAD"?"shared":"view-only"}a(Pt,"relayVerdict");function ce(){for(const t of S()){if(!t.shared?.size)continue;const e=Mt(t.board);for(const r of t.shared)e.includes(r.login)||(t.shared.delete(r),r.res.destroy())}}a(ce,"endUnshared");async function le(t){t.https=ae(t);const e=t.sockets=new Set,r=a(()=>{t.visited=o()},"visit"),s=a(n=>{e.add(n),n.on("data",r),n.on("close",()=>e.delete(n))},"track"),i=t.shared=new Set;if(t.relay=Ot.createServer(async(n,c)=>{const w=await Pt(n,t.board);if(w!=="owner"&&w!=="shared"){Fe(n,c,w);return}if(w==="shared"){const v={login:n.headers["tailscale-user-login"],res:c};i.add(v),c.on("close",()=>i.delete(v))}const T=Object.fromEntries(Object.entries(n.headers).filter(([v])=>!Qt.includes(v))),h=Ot.request({host:"127.0.0.1",port:t.port,method:n.method,path:n.url,headers:T},v=>{c.writeHead(v.statusCode,t.key===L?Le(v.headers):v.headers),v.on("data",r),v.pipe(c),v.on("close",()=>{v.complete||c.destroy()})});c.on("close",()=>{c.writableFinished||h.destroy()}),h.on("error",()=>{c.headersSent?c.destroy():c.writeHead(502).end()}),n.pipe(h)}),t.relay.on("connection",n=>{r(),s(n)}),t.relay.on("upgrade",async(n,c,w)=>{const T=await Pt(n,t.board);if(T!=="owner"){Re(c,T==="shared"?"view-only":T);return}s(De({req:n,socket:c,head:w,port:t.port,path:n.url,replace:Qt}))}),await new Promise((n,c)=>{t.relay.once("error",c),t.relay.listen(0,"127.0.0.1",n)}),!t.stopped){t.exposed=!0;try{await st(["serve","--bg",`--https=${t.https}`,`http://127.0.0.1:${t.relay.address().port}`])}catch(n){throw new Error(`Couldn't open try port ${t.https}: ${(n.stderr||n.message).trim()}`)}}}a(le,"openPort");async function de(t){t.closing=!0,await t.opening?.catch(()=>{});const e=[...m.values()].some(r=>r!==t&&r.exposed&&r.https===t.https);if(t.exposed&&!H&&!e&&await At(t.https),t.relay){for(const r of t.sockets)r.destroy();await new Promise(r=>t.relay.close(r))}t.keepPort||U.delete(D(t.board,t.key))}a(de,"closePort");async function ue(){const t=S().filter(r=>r.ready&&r.relay&&!r.closing);if(!t.length)return;let e;try{e=JSON.parse((await st(["serve","status","--json"])).stdout)}catch(r){l(`try ports check: ${(r.stderr||r.message).trim()}`);return}for(const r of t){if(r.stopped)continue;const s=`http://127.0.0.1:${r.relay.address().port}`,i=Object.entries(e?.Web||{}).find(([n])=>n.endsWith(`:${r.https}`))?.[1];if(i?.Handlers?.["/"]?.Proxy!==s){l(`try port ${r.https}: its serve mapping was ${i?"changed":"gone"}; serving it again`);try{await st(["serve","--bg",`--https=${r.https}`,s])}catch(n){l(`try port ${r.https}: could not serve it again: ${(n.stderr||n.message).trim()}`)}}}}a(ue,"reserve");function nt(t=V()){if(ut)return ut;$t(t);for(const e of z(t))C(e);return ut=(async()=>{for(const e of z(t))await ne(e).catch(r=>l(`try startup sweep /${e.route}: ${r.message}`));try{const{stdout:e}=await st(["serve","status","--json"]),r=JSON.parse(e);for(const s of Object.keys(r?.TCP||{}))Tt(s)&&![...m.values()].some(i=>i.https===Number(s))&&await At(s)}catch(e){l(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),ut}a(nt,"initialize");async function Ct(t,e,r,s){const{board:i,key:n}=t;if(K(i,n,{state:"failed",port:void 0,https:void 0}),!R(n))return;let c="",w=!1;try{for await(const v of Lt({input:j.createReadStream(t.logFile),crlfDelay:1/0}))w||=He.test(v),v.trim()&&(c=v)}catch(v){c=`could not read the build log: ${v.message}`}const T=t.port||(C(i).state.kind||(i.files.some(({itemFile:v})=>v.items.some(x=>x.install?.url))?"app":"web"))==="web"?"Serve":"Build",h=w?"unlock the login keychain on the host":s?.message||r&&`stopped by ${r}`||c;await Yt(i.dir,n,"run-failed",{kind:T,code:e??1,why:h})}a(Ct,"failure");function pt(t){return t.ending||(t.ending=ee(t.child.pid)),t.ending}a(pt,"terminate");async function ot(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await pt(t),await t.done}a(ot,"halt");async function tt(t,e=!1){await ot(t,e),K(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||gt.delete(D(t.board,t.key))}a(tt,"stopRun");async function Nt(t,e,r,s={}){if(!r)throw new _("This card has no valid worktree.");const i=Vt(r,t.build_target),n=xe(i);if(n)throw new _(n);if(!j.realpathSync(i).startsWith(j.realpathSync(r)+O.sep))throw new _("Build script must be inside the worktree.");const c=await I(r,"rev-parse","HEAD");if(H)throw new _("The hub is stopping. Try again after it restarts.",503);const{state:w}=C(t),T=_t(w,e),h={...R(e)?T:{},...s,sha:c,script:!0,state:"running",...R(e)?{at:ft(),head:c}:{},...e===L?{since:ft()}:{}};delete h.port,delete h.https;const v=qt(t,e);j.mkdirSync(O.dirname(v),{recursive:!0});const x=j.openSync(v,"w",384);j.fchmodSync(x,384);const b=ge(i,[e,r],{cwd:r,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});oe(w,e,h);const F=D(t,e),u={child:b,board:t,key:e,worktree:r,sha:c,at:s.at,logFile:v,visited:o(),stopped:!1,port:null,ready:!1};ie(u),m.set(F,u),gt.set(F,c);let B,P;const Y=a(N=>{P||=new Error(N),pt(u)},"fail");b.on("error",N=>{B=N}),b.stdout.on("data",N=>j.writeSync(x,N)),b.stderr.on("data",N=>j.writeSync(x,N));const J=Lt({input:b.stdout,crlfDelay:1/0});return J.on("line",N=>{const at=/^port (\d{1,5})$/.exec(N);if(!at||u.port||u.stopped)return;const G=Number(at[1]);if(G<1||G>65535){Y("Build script printed an invalid port.");return}u.port=G,u.group.port=G,A(t),clearTimeout(u.deadline);let Q,St=!1;const Ht=a(async()=>{if(!(St||u.stopped||u.ready||b.exitCode!==null||b.signalCode!==null)){St=!0;try{if(await d(G)){if(u.stopped||P||b.exitCode!==null||b.signalCode!==null||(u.opening=le(u),await u.opening,u.stopped||P||b.exitCode!==null||b.signalCode!==null))return;u.ready=!0,u.visited=o(),w.kind="web",K(t,e,{state:"ready",port:G,https:u.https}),clearInterval(u.poll),e==="main"&&await vt(t)}else o()>=Q&&Y("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{St=!1}}},"check"),Wt=a(()=>{u.stopped||(Q=o()+6e4,u.poll=setInterval(()=>Ht().catch(ct=>Y(ct.message)),250),Ht().catch(ct=>Y(ct.message)))},"probeFrom");e==="main"&&p.get(t.dir)?.port===G?vt(t).then(Wt,ct=>Y(ct.message)):Wt()}),u.deadline=setTimeout(()=>Y("No port line within 60 minutes."),60*6e4),b.once("exit",()=>{pt(u)}),u.done=new Promise(N=>b.once("close",(at,G)=>{clearTimeout(u.deadline),clearInterval(u.poll),J.close(),j.closeSync(x),(async()=>{if(await pt(u),await de(u),se(u),H||await jt(t,e,u.port),u.finished=!0,u.stopped)return;if(p.get(t.dir)===u){p.delete(t.dir),K(t,e,{serving:void 0});return}const Q=e===L&&!u.port?new Error('Build script exited without printing "port <n>".'):null;P||B||at!==0||u.port&&!u.ready||Q?await Ct(u,at??1,G,P||B||(u.port&&!u.ready?new Error("Build script exited before its port answered."):Q)):u.ready?(e==="main"?(delete w.main.state,delete w.main.port,delete w.main.https):It(w,e),A(t)):(w.kind="app",K(t,e,{state:"ready"}))})().catch(Q=>l(`build /${t.route} ${e}: ${Q.message}`)).finally(()=>{m.get(F)===u&&m.delete(F),N()})})),l(`build /${t.route} ${e}: started; log ${v}`),{status:"started"}}a(Nt,"launch");async function vt(t){const e=p.get(t.dir);e&&(p.delete(t.dir),K(t,"main",{serving:void 0}),await ot(e))}a(vt,"retire");async function Dt(t,e,r){const s=D(t,"main");if(E.has(s))throw new _("A build is already running for main.",409);E.add(s);try{const i=m.get(s);i?.ready&&!i.ending?(m.delete(s),p.set(t.dir,i)):i&&await tt(i,!0);const n=p.get(t.dir);n&&(r={...r,serving:{sha:n.sha,at:n.at,https:n.https}});const c=a(T=>O.join(Gt.homedir(),".claude","kickflip","try",`${t.route}-main${T}`),"worktreeOf"),w=c(n?.worktree===c("-a")?"-b":"-a");return j.mkdirSync(O.dirname(w),{recursive:!0}),mt(c(""))&&await I(e,"worktree","remove","--force",c("")),mt(w)?await I(w,"checkout","--detach","--force",r.sha):(await I(e,"worktree","prune"),await I(e,"worktree","add","--detach",w,r.sha)),await Nt(t,"main",w,r)}catch(i){throw i.status!==503&&(C(t).state.main={...r,state:"failed"},xt(t,"main",i),A(t)),i}finally{E.delete(s)}}a(Dt,"launchMain");async function bt(t,{manifest:e=V()}={}){if(await nt(e),$t(e),H)throw new _("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(h=>!["route","key"].includes(h))||typeof t.route!="string"||typeof t.key!="string")throw new _("Send only the board route and card key.");const{route:r,key:s}=t;if(R(s)&&(!/^[\w.-]+\/[\w.-]+$/.test(s)||s.split("/").some(h=>h==="."||h==="..")))throw new _("Card key must be <file>/<id>.");const i=z(e).find(h=>h.route===r);if(!i)throw new _(`/${r} is not a board.`,404);if(R(s)&&!i.files.some(({slug:h,itemFile:v})=>v.items.some(x=>`${h}/${x.id}`===s)))throw new _("There is no item with this card key.",404);const n=await g();if(n)throw new _(n,409);const c=D(i,s),w=m.get(c);if(E.has(c)||w&&!w.ready&&!w.finished)throw new _(`A build is already running for ${s==="main"?"main":s===L?"the prototype site":"this card"}.`,409);if(s==="main"){const h=C(i).state.main;if(h?.state==="running")throw new _("A build is already running for main.",409);if(h?.state!=="failed"||!h.sha)throw new _("Main has no failed build to retry.",409);const{sha:v,branch:x,slug:b,at:F,script:u}=h;return Dt(i,et(r,e).repo,{sha:v,branch:x,slug:b,at:F,script:u,...i.build_target?{target:i.build_target}:{}})}E.add(c);const T=M.delete(c);try{m.has(c)&&await tt(m.get(c),!0);const h=s===L?Ft(i):await re(i,s,e);return await Nt(i,s,h)}catch(h){throw U.delete(c),T&&h.status!==503&&K(i,s,{state:void 0,sha:void 0}),h}finally{E.delete(c)}}a(bt,"start");const Ft=a(t=>{const e=Ae(t.route);return mt(e)&&j.statSync(e).isDirectory()?j.realpathSync(e):""},"baseWorktree");async function fe(t,e){const{state:r}=C(t),s=D(t,L),i=m.get(s),n=(await _e(t.dir)).length>0&&Ft(t);if(!n){i&&await tt(i),r.prototypes&&(r.prototypes=null,A(t));return}if(i?.ready&&!i.stopped&&!E.has(s)){const c=await I(n,"rev-parse","HEAD");if(c===r.prototypes?.sha)return;l(`try /${t.route} prototypes: the base moved to ${c.slice(0,7)}; restarting its serve`)}else if(i||E.has(s)||r.prototypes?.state==="failed"||await g())return;try{await bt({route:t.route,key:L},{manifest:e})}catch(c){if([409,503].includes(c.status))return;throw r.prototypes={state:"failed"},xt(t,L,c),A(t),c}}a(fe,"serveBase");function Rt(t,e,{manifest:r=V()}={}){const s=z(r).find(n=>n.route===t);if(!s)return null;const i=[...m.values()].find(n=>n.board.dir===s.dir&&n.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&n.ready&&!n.stopped&&n.child.exitCode===null&&n.child.signalCode===null);return i?{key:i.key,port:i.port}:null}a(Rt,"lookup");async function kt({manifest:t=V(),routes:e}={}){if(await nt(t),$t(t),H)return W;if(W&&e){if(!Z){const r={routes:new Set,manifest:t};r.done=W.catch(()=>{}).then(()=>(Z=null,kt({manifest:r.manifest,routes:[...r.routes]}))),Z=r}Z.manifest=t;for(const r of e)Z.routes.add(r);return Z.done}return W||(W=(async()=>{await ue();const r=z(t),s=lt(be,{}).prs||{};for(const[i,n]of[...p])r.some(c=>c.dir===i)||(p.delete(i),await ot(n));for(const i of[...m.values()])if(!r.some(n=>n.dir===i.board.dir)||R(i.key)&&i.ready&&o()-i.visited>=We){await tt(i);const{state:n}=C(i.board);It(n,i.key),A(i.board)}for(const i of r){if(H)return;if(e&&!e.includes(i.route))continue;const{state:n}=C(i),c=et(i.route,t),w=c.branch||"main",T=ke(c);for(const{slug:h,itemFile:v}of i.files)for(const x of v.items){const b=`${h}/${x.id}`,F=Me(x,i.github||v.repo).map(u=>s[Ce(u)]);if(!(x.lane==="done"||F.length&&F.every(u=>["MERGED","CLOSED"].includes(u?.state))))try{const u=D(i,b),B=await re(i,b,t);if(B){const J=await I(B,"rev-parse","HEAD");let N=0;try{N=Number(await I(B,"rev-list","--count",`${T}..HEAD`))}catch{}Object.assign(n.cards[b]||={},{head:J,ahead:N,script:mt(Vt(B,i.build_target))}),A(i)}else if(!M.has(u))continue;const P=n.cards[b];if((M.has(u)||i.auto_build&&P.script&&P.ahead>0&&(P.sha||gt.get(u))!==P.head&&P.state!=="running")&&!E.has(u))try{await bt({route:i.route,key:b},{manifest:t})}catch(J){if([409,503].includes(J.status))continue;throw K(i,b,{sha:P.head}),await Ct({board:i,key:b,logFile:xt(i,b,J)},1),J}}catch(u){l(`try /${i.route} ${b}: ${u.message}`)}}await fe(i,t).catch(h=>l(`try /${i.route} prototypes: ${h.message}`));try{if(!c.repo||await g())continue;const h=await I(c.repo,"rev-parse",T);let v=!1;try{await I(c.repo,"cat-file","-e",`${h}:${zt(i.build_target)}`),v=!0}catch{}if(!v){const P=m.get(D(i,"main"));P&&await tt(P),await vt(i),n.main=null,A(i);continue}const x=n.main;if(x?.state==="running"||x?.sha===h&&(x.target||null)===(i.build_target||null)&&(x.state==="failed"||x.state==="ready"&&(n.kind!=="web"||Rt(i.route,"main",{manifest:t}))))continue;const b=D(i,"main");if(E.has(b))continue;const F=await I(c.repo,"log","-1","--format=%cI",h);let u=i.github;if(!u)try{u=Jt(await I(c.repo,"remote","get-url","origin"))}catch{}const B={sha:h,branch:w,slug:u||"",at:F,script:v,...i.build_target?{target:i.build_target}:{}};await Dt(i,c.repo,B)}catch(h){l(`try /${i.route} main: ${h.message}`)}}})().finally(()=>{W=null}),W)}a(kt,"tick");function Bt(t,e){if(it.has(t.dir))return it.get(t.dir);const r=(async()=>{const s=et(t.route,e),i=s.branch||"main",{state:n}=C(t);n.pull={...n.pull,running:!0},A(t);let c,w;try{c=await(s.local?Qe:Ye)(s.repo,i)}catch(T){w=T.message}if(c===null?delete n.pull:n.pull={at:ft(),...w?{error:w}:{}},A(t),c===null)return null;if(w)return l(`pull /${t.route}: ${w}`),{commits:0,error:w};if(c.commits){l(`pull /${t.route}: ${i} ${s.local?"fast-forwarded":"reset"} ${c.commits} commit${c.commits===1?"":"s"} to ${c.to.slice(0,7)}`);try{await f(t,"main-pulled",{branch:i,...c})}catch(T){l(`pull /${t.route}: could not send main-pulled (${T.message})`)}}return{commits:c.commits}})().finally(()=>it.delete(t.dir));return it.set(t.dir,r),r}a(Bt,"pullMain");async function pe({manifest:t=V(),routes:e}={}){await nt(t);const r=[];for(const s of z(t)){if(H)break;if(!(e&&!e.includes(s.route)))try{if(!et(s.route,t).repo)continue;(await Bt(s,t))?.commits&&r.push(s.route)}catch(i){l(`pull /${s.route} failed: ${i.message}`)}}return r}a(pe,"pullMains");async function he(t,{manifest:e=V()}={}){if(await nt(e),H)throw new _("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(n=>n!=="route")||typeof t.route!="string")throw new _("Send only the board route.");const r=z(e).find(n=>n.route===t.route);if(!r)throw new _(`/${t.route} is not a board.`,404);const s=et(r.route,e),i=await Bt(r,e);if(!i)throw new _("This board\u2019s folder has no origin to pull from.",409);return(i.commits||!s.local)&&kt({manifest:e,routes:[r.route]}).catch(n=>l(`try /${r.route} main: ${n.message}`)),i}a(he,"pull");async function me(){H=!0;const t=Promise.all([...m.values()].map(e=>e.ready?tt(e):ot(e)).concat([...p.values()].map(e=>ot(e))));await Promise.all([t,W,...it.values()])}return a(me,"stop"),{start:bt,tick:kt,stop:me,lookup:Rt,initialize:nt,pull:he,pullMains:pe,endUnshared:ce}}a(ze,"createTryManager");const wt=1e4,yt=a(async(o,...d)=>(await dt("git",["-C",o,...d],{encoding:"utf8",timeout:wt,env:Oe()})).stdout,"originGit");async function Ye(o,d,{run:y=yt}={}){const l=a(async(...p)=>(await y(...p)).trim(),"git"),g=a(p=>l(o,"rev-parse","--verify","--quiet",`${p}^{commit}`).catch(()=>""),"tip"),f=`refs/remotes/origin/${d}`,k=await g(`refs/heads/${d}`)||await g(f);try{await l(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:${f}`),await l(o,"checkout","--quiet","--force","-B",d,f),await l(o,"clean","--quiet","-fd")}catch(p){throw new Error(`can\u2019t pull: ${p.killed?`git fetch timed out after ${wt/1e3} s`:Et(p.stderr,{repo:o}).error}`)}const $=await g(f),m=k&&k!==$?Number(await l(o,"rev-list","--count",`${k}..${$}`)):0;return{from:k||$,to:$,commits:m}}a(Ye,"resetBranch");async function Qe(o,d,{run:y=yt}={}){const l=a(async(...f)=>(await y(...f)).trim(),"git"),g=a(f=>{throw new Error(`can\u2019t pull: ${f}`)},"refuse");try{await l(o,"config","--get","remote.origin.url")}catch{return null}try{await l(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:refs/remotes/origin/${d}`)}catch(f){g(f.killed?`git fetch timed out after ${wt/1e3} s`:Et(f.stderr,{repo:o}).error)}try{const f=await l(o,"rev-parse","--verify",`refs/heads/${d}^{commit}`),k=await l(o,"rev-parse","--verify",`refs/remotes/origin/${d}^{commit}`),$=a((S,E)=>l(o,"merge-base","--is-ancestor",S,E).then(()=>!0,M=>{if(M.code===1)return!1;throw M}),"isAncestor");if(f===k||await $(k,f))return{from:f,to:f,commits:0};await $(f,k)||g(`${d} has split from origin`);const m=Number(await l(o,"rev-list","--count",`${f}..${k}`)),p=(await l(o,"worktree","list","--porcelain")).split(`
1
+ var Se=Object.defineProperty;var a=(o,d)=>Se(o,"name",{value:d,configurable:!0});import S from"node:fs";import T from"node:path";import Jt from"node:os";import Tt from"node:http";import{execFile as Oe,spawn as xe}from"node:child_process";import{createInterface as zt}from"node:readline";import{promisify as Ee}from"node:util";import{listBoards as J,readManifest as Q,readJSON as ft,saveJSON as Yt,appendEvent as Te,GITHUB_STATE as je,REPO_FETCH as Qt}from"./boards.mjs";import{boardRepo as et,githubFromOrigin as Vt,mainRefOf as _e}from"./board-repo.mjs";import{gitReachFault as jt,remoteFromInput as Ie,remoteGitEnv as Ae}from"./repos.mjs";import{readiness as Me}from"./readiness.mjs";import{tailscaleBin as Pe}from"../shared/tailscale.mjs";import{buildScriptError as Ce,buildScriptOf as qt}from"../board/build-script.mjs";import{appendStatus as Xt,recordedWorktree as Ne,servedRoundsOpen as Fe,setItem as De}from"../board/store.mjs";import{BASE_KEY as U,baseWorktreeOf as Be}from"../board/prototype-base.mjs";import{cardPRs as Re,hasPR as He,prKeyOf as We}from"../shared/card-prs.mjs";import{hubGate as Ge}from"../board/on-host.mjs";import{pipeUpgrade as Le,refuse as Ue,refuseUpgrade as Ke}from"./front-door.mjs";import{shareOf as Je}from"./share.mjs";class A extends Error{static{a(this,"BuildError")}constructor(d,h=400){super(d),this.status=h}}const rt=Ee(Oe),M=a(async(o,...d)=>(await rt("git",["-C",o,...d],{encoding:"utf8",timeout:3e3})).stdout.trim(),"git"),ze=/errSecInternalComponent|keychain[^\n]*\blocked\b|\blocked\b[^\n]*keychain/i,V={first:8500,last:8599},_t=a(o=>/^\d+$/.test(String(o))&&Number(o)>=V.first&&Number(o)<=V.last,"isTryPort"),Ye=30*6e4,Zt=["tailscale-user-login","tailscale-user-name","tailscale-user-profile-pic","x-forwarded-for"],Qe=["x-frame-options"],Ve=a(o=>{const d=Object.fromEntries(Object.entries(o).filter(([h])=>!Qe.includes(h)));if(typeof d["content-security-policy"]=="string"){const h=d["content-security-policy"].split(";").filter(c=>!/^\s*frame-ancestors\b/i.test(c)).join(";").trim();h?d["content-security-policy"]=h:delete d["content-security-policy"]}return d},"withoutFraming"),yt={main:"main",[U]:"prototypes"},H=a(o=>!Object.hasOwn(yt,o),"isCard"),te=a((o,d)=>T.join(o,qt(d)),"scriptAt"),gt=a(o=>S.existsSync(o),"exists"),ee=a((o,d)=>T.join(o.dir,"data","try",`${d.split("/").at(-1)}.log`),"logFileOf");function It(o,d,h){const c=ee(o,d);return S.mkdirSync(T.dirname(c),{recursive:!0}),S.writeFileSync(c,h.message+`
2
+ `,{mode:384}),S.chmodSync(c,384),c}a(It,"failureLog");const re=a(o=>new Promise(d=>{const h=Tt.get({host:"127.0.0.1",port:o,path:"/",timeout:1e3},c=>{c.destroy(),d(!0)});h.on("timeout",()=>h.destroy()),h.on("error",()=>d(!1))}),"probe"),ie=a((o,d)=>{try{process.kill(-o,d)}catch{}},"signalGroup"),se=a(o=>{try{return process.kill(-o,0),!0}catch{return!1}},"groupUp"),At=a(o=>new Promise(d=>{const h=a(()=>{clearTimeout(c),clearInterval(y),d()},"finish");ie(o,"SIGTERM");let c=setTimeout(()=>{ie(o,"SIGKILL"),c=setTimeout(h,1e3)},4e3);const y=setInterval(()=>{se(o)||h()},20)}),"endGroup");async function qe(o){const{stdout:d}=await rt("ps",["-axo","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3}),h=d.split(`
3
+ `).map(c=>/^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(c)).filter(c=>c&&Number(c[2])===o).map(c=>({pid:c[1],command:c[3],cwd:""}));return h.length&&await ne(h,["-a","-d","cwd","-Fpn","-p",h.map(c=>c.pid).join(",")]),h}a(qe,"groupMembers");async function ne(o,d){let h="";try{h=(await rt("lsof",d,{encoding:"utf8",timeout:1e4,maxBuffer:64*1024*1024})).stdout}catch(u){h=u.stdout||""}const c=new Map(o.map(u=>[u.pid,u]));let y;for(const u of h.split(`
4
+ `))u[0]==="p"?y=c.get(u.slice(1)):u[0]==="n"&&y&&(y.cwd=u.slice(1));for(const u of o){if(!u.cwd&&S.existsSync("/proc"))try{u.cwd=S.readlinkSync(`/proc/${u.pid}/cwd`)}catch{}u.cwd=u.cwd.replace(/ \(deleted\)$/,"")}}a(ne,"readCwds");async function Xe(o){const{stdout:d}=await rt("ps",["-axo","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3,maxBuffer:67108864}),h=d.split(`
5
+ `).map(c=>/^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(c)).filter(Boolean).map(c=>({pid:c[1],pgid:Number(c[2]),command:c[3],cwd:""}));return await ne(h,["-d","cwd","-Fpn"]),h.filter(c=>oe(c,o))}a(Xe,"processesIn");const Ze=a((o,d)=>o===d||o.startsWith(d+T.sep),"insideDir"),oe=a((o,d)=>Ze(o.cwd,d)||o.command.includes(d+T.sep)||o.command.endsWith(d),"inWorktree");async function ae(o,d,h){const c=await Ne(o.dir,d),[y,u]=d.split("/"),k=o.files.find(O=>O.slug===y)?.itemFile,g=c?.path||k?.items.find(O=>O.id===u)?.worktree;if(typeof g!="string"||!g||g.split(T.sep).includes(".."))return"";let m=c?.repo;const p=(o.github||k?.repo||"").toLowerCase();if(!m&&p){const O=[o.owner?.repo,o.dir,...Object.values(h?.sites||{}).map(F=>F.owner?.repo)];for(const F of new Set(O.filter(x=>typeof x=="string"&&T.isAbsolute(x))))try{if(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(await M(F,"remote","get-url","origin"))?.[1].toLowerCase()===p){m=await M(F,"rev-parse","--show-toplevel");break}}catch{}}if(!c?.path&&!m||c?.path&&!T.isAbsolute(g))return"";try{const O=[T.join(S.realpathSync(Jt.homedir()),".claude","kickflip","worktrees")];if(typeof m=="string"&&T.isAbsolute(m))try{O.push(T.join(S.realpathSync(m),".claude","worktrees"))}catch{}const F=T.resolve(m||"/",g),x=S.realpathSync(F);return O.some(z=>F.startsWith(z+T.sep)&&x.startsWith(z+T.sep))&&S.statSync(x).isDirectory()?x:""}catch{return""}}a(ae,"installWorktree");function tr({now:o=Date.now,answers:d=re,lingers:h=re,log:c=console.log,waiting:y=a(async()=>null,"waiting"),event:u=Te,gate:k=Ge}={}){const g=new Map,m=new Map,p=new Map,O=new Set,F=a(()=>[...m.values(),...p.values()],"live"),x=new Set,z=new Set,it=new Map,st=new Map,bt=new Map,nt=new Map;let W=!1,G=null,X=null,pt=null;const ht=a(()=>new Date(o()).toISOString(),"stamp");function C(t){if(g.has(t.dir))return g.get(t.dir);const e=T.join(t.dir,"data","try.json"),r=ft(e,{kind:null,main:null,cards:{}}),s={...Array.isArray(r.groups)&&r.groups.length?{groups:r.groups}:{},..._t(r.main_port)?{main_port:r.main_port}:{},kind:r.kind||null,main:r.main||null,cards:{...r.cards},...r.pull?{pull:{...r.pull,running:void 0}}:{}},i={state:s,file:e,written:JSON.stringify(r)};g.set(t.dir,i),s.main_port&&st.set(t.dir,s.main_port);const n=Object.entries(s.cards).filter(([,l])=>l.state==="running");for(const[l]of n)z.add(B(t,l));return s.kind==="web"?(s.cards=Object.fromEntries(n),s.main&&s.main.state!=="failed"&&(delete s.main.state,delete s.main.port,delete s.main.https)):s.main?.state==="running"&&(delete s.main.state,delete s.main.sha),s.main&&delete s.main.serving,r.prototypes?.state==="failed"&&(s.prototypes=r.prototypes),P(t),i}a(C,"stateOf");function P(t){const e=g.get(t.dir),r=JSON.stringify(e.state);r!==e.written&&(Yt(e.file,e.state),e.written=r)}a(P,"persist");function ce(t){const{state:e}=C(t.board);t.group={pgid:t.child.pid,key:t.key,worktree:t.worktree},(e.groups||=[]).push(t.group),P(t.board)}a(ce,"recordGroup");function le(t){const{state:e}=C(t.board);e.groups?.includes(t.group)&&(e.groups=e.groups.filter(r=>r!==t.group),e.groups.length||delete e.groups,P(t.board))}a(le,"forgetGroup");async function de(t){const{state:e}=C(t),r=e.groups||[];delete e.groups,P(t);for(const s of r){if(!se(s.pgid))continue;let i=[];try{i=await qe(s.pgid)}catch(n){c(`try startup sweep: could not read group ${s.pgid}: ${n.message}`)}i.some(n=>oe(n,s.worktree))&&(await At(s.pgid),c(`stopped leftover try /${t.route} ${s.key} (pgid ${s.pgid})`),await Mt(t,s.key,s.port))}}a(de,"sweepGroups");async function Mt(t,e,r){if(!r||!await h(r))return;const s=`Port ${r} still answers after the run was stopped.`;H(e)&&await Xt(t.dir,e,"run-failed",{kind:"Serve",code:1,why:s}).catch(i=>c(`build /${t.route} ${e}: ${i.message}`)),c(`build /${t.route} ${e}: ${s}`)}a(Mt,"checkQuiet");const B=a((t,e)=>`${t.dir}\0${e}`,"runKey"),Pt=a((t,e)=>H(e)?t.cards[e]:t[yt[e]],"entryOf"),ue=a((t,e,r)=>{H(e)?t.cards[e]=r:t[yt[e]]=r},"setEntry"),Ct=a((t,e)=>{H(e)?delete t.cards[e]:t[yt[e]]=null},"dropEntry");function K(t,e,r){const{state:s}=C(t),i=Pt(s,e);if(!i)return;const n=Object.hasOwn(r,"state")&&r.state!==i.state;Object.assign(i,r,H(e)&&n?{at:ht()}:{}),P(t)}a(K,"update");function kt(t){const e=[...Object.values(t?.sites||{}),...Object.values(t?.closed||{}).map(r=>r.site)];for(const r of e){if(!r?.dir)continue;const s=ft(T.join(r.dir,"data","try.json"),{}).main_port;_t(s)&&st.set(r.dir,Number(s))}}a(kt,"reserveMains");function fe(t){const{board:e,key:r}=t,s=B(e,r),i=r==="main"?st.get(e.dir):it.get(s);if(i)return i;const n=new Set([...st.values(),...it.values()]);let l=V.first;for(;n.has(l)&&l<=V.last;)l++;if(l>V.last)throw new Error(`No try ports available (${V.first}\u2013${V.last}). Stop a served card and retry.`);return r==="main"?(st.set(e.dir,l),C(e).state.main_port=l,P(e)):it.set(s,l),l}a(fe,"publicPort");const ot=a(async t=>{const e=Pe();if(!e)throw new Error("Tailscale isn't installed on this host");return rt(e,t,{encoding:"utf8",timeout:1e4})},"tailscale");async function Nt(t){try{await ot(["serve",`--https=${t}`,"off"])}catch(e){c(`try port ${t}: could not close port: ${(e.stderr||e.message).trim()}`)}}a(Nt,"removePort");const Ft=a(t=>Je(ft(T.join(t.dir,"board.json"),{})),"sharedWith");async function Dt(t,e){const r=await k(t),s=t.headers["tailscale-user-login"];return r!=="other"||!s||!Ft(e).includes(s)?r:t.method==="GET"||t.method==="HEAD"?"shared":"view-only"}a(Dt,"relayVerdict");function pe(){for(const t of F()){if(!t.shared?.size)continue;const e=Ft(t.board);for(const r of t.shared)e.includes(r.login)||(t.shared.delete(r),r.res.destroy())}}a(pe,"endUnshared");async function he(t){t.https=fe(t);const e=t.sockets=new Set,r=a(()=>{t.visited=o()},"visit"),s=a(n=>{e.add(n),n.on("data",r),n.on("close",()=>e.delete(n))},"track"),i=t.shared=new Set;if(t.relay=Tt.createServer(async(n,l)=>{const $=await Dt(n,t.board);if($!=="owner"&&$!=="shared"){Ue(n,l,$);return}if($==="shared"){const v={login:n.headers["tailscale-user-login"],res:l};i.add(v),l.on("close",()=>i.delete(v))}const b=Object.fromEntries(Object.entries(n.headers).filter(([v])=>!Zt.includes(v))),w=Tt.request({host:"127.0.0.1",port:t.port,method:n.method,path:n.url,headers:b},v=>{l.writeHead(v.statusCode,t.key===U?Ve(v.headers):v.headers),v.on("data",r),v.pipe(l),v.on("close",()=>{v.complete||l.destroy()})});l.on("close",()=>{l.writableFinished||w.destroy()}),w.on("error",()=>{l.headersSent?l.destroy():l.writeHead(502).end()}),n.pipe(w)}),t.relay.on("connection",n=>{r(),s(n)}),t.relay.on("upgrade",async(n,l,$)=>{const b=await Dt(n,t.board);if(b!=="owner"){Ke(l,b==="shared"?"view-only":b);return}s(Le({req:n,socket:l,head:$,port:t.port,path:n.url,replace:Zt}))}),await new Promise((n,l)=>{t.relay.once("error",l),t.relay.listen(0,"127.0.0.1",n)}),!t.stopped){t.exposed=!0;try{await ot(["serve","--bg",`--https=${t.https}`,`http://127.0.0.1:${t.relay.address().port}`])}catch(n){throw new Error(`Couldn't open try port ${t.https}: ${(n.stderr||n.message).trim()}`)}}}a(he,"openPort");async function me(t){t.closing=!0,await t.opening?.catch(()=>{});const e=[...m.values()].some(r=>r!==t&&r.exposed&&r.https===t.https);if(t.exposed&&!W&&!e&&await Nt(t.https),t.relay){for(const r of t.sockets)r.destroy();await new Promise(r=>t.relay.close(r))}t.keepPort||it.delete(B(t.board,t.key))}a(me,"closePort");async function we(){const t=F().filter(r=>r.ready&&r.relay&&!r.closing);if(!t.length)return;let e;try{e=JSON.parse((await ot(["serve","status","--json"])).stdout)}catch(r){c(`try ports check: ${(r.stderr||r.message).trim()}`);return}for(const r of t){if(r.stopped)continue;const s=`http://127.0.0.1:${r.relay.address().port}`,i=Object.entries(e?.Web||{}).find(([n])=>n.endsWith(`:${r.https}`))?.[1];if(i?.Handlers?.["/"]?.Proxy!==s){c(`try port ${r.https}: its serve mapping was ${i?"changed":"gone"}; serving it again`);try{await ot(["serve","--bg",`--https=${r.https}`,s])}catch(n){c(`try port ${r.https}: could not serve it again: ${(n.stderr||n.message).trim()}`)}}}}a(we,"reserve");function at(t=Q()){if(pt)return pt;kt(t);for(const e of J(t))C(e);return pt=(async()=>{for(const e of J(t))await de(e).catch(r=>c(`try startup sweep /${e.route}: ${r.message}`));try{const{stdout:e}=await ot(["serve","status","--json"]),r=JSON.parse(e);for(const s of Object.keys(r?.TCP||{}))_t(s)&&![...m.values()].some(i=>i.https===Number(s))&&await Nt(s)}catch(e){c(`try startup cleanup: ${(e.stderr||e.message).trim()}`)}})(),pt}a(at,"initialize");async function Bt(t,e,r,s){const{board:i,key:n}=t;if(K(i,n,{state:"failed",port:void 0,https:void 0}),!H(n))return;let l="",$=!1;try{for await(const v of zt({input:S.createReadStream(t.logFile),crlfDelay:1/0}))$||=ze.test(v),v.trim()&&(l=v)}catch(v){l=`could not read the build log: ${v.message}`}const b=t.port||(C(i).state.kind||(i.files.some(({itemFile:v})=>v.items.some(j=>j.install?.url))?"app":"web"))==="web"?"Serve":"Build",w=$?"unlock the login keychain on the host":s?.message||r&&`stopped by ${r}`||l;await Xt(i.dir,n,"run-failed",{kind:b,code:e??1,why:w})}a(Bt,"failure");function mt(t){return t.ending||(t.ending=At(t.child.pid)),t.ending}a(mt,"terminate");async function ct(t,e=!1){t.keepPort=e,t.stopped=!0,clearTimeout(t.deadline),clearInterval(t.poll),await mt(t),await t.done}a(ct,"halt");async function Z(t,e=!1){await ct(t,e),K(t.board,t.key,{state:void 0,port:void 0,https:void 0,...t.ready?{}:{sha:void 0}}),t.ready||bt.delete(B(t.board,t.key))}a(Z,"stopRun");async function St(t,e,r,s={},i=""){if(!r)throw new A("This card has no valid worktree.");const n=te(r,t.build_target),l=Ce(n);if(l)throw new A(l);if(!S.realpathSync(n).startsWith(S.realpathSync(r)+T.sep))throw new A("Build script must be inside the worktree.");const $=await M(r,"rev-parse","HEAD");if(W)throw new A("The hub is stopping. Try again after it restarts.",503);const{state:b}=C(t),w=Pt(b,e),v={...H(e)?w:{},...s,sha:$,script:!0,state:"running",...H(e)?{at:ht(),head:$}:{},...e===U?{since:ht()}:{}};delete v.port,delete v.https;const j=ee(t,e);S.mkdirSync(T.dirname(j),{recursive:!0});const I=S.openSync(j,"w",384);S.fchmodSync(I,384),i&&S.writeSync(I,i);const _=xe(n,[e,r],{cwd:r,detached:!0,stdio:["ignore","pipe","pipe"],env:{...process.env,KICKFLIP_BOARD:t.route,HOST:"127.0.0.1"}});ue(b,e,v);const E=B(t,e),f={child:_,board:t,key:e,worktree:r,sha:$,at:s.at,metadata:s,logFile:j,visited:o(),stopped:!1,port:null,ready:!1};ce(f),m.set(E,f),bt.set(E,$);let N,tt;const D=a(R=>{tt||=new Error(R),mt(f)},"fail");_.on("error",R=>{N=R}),_.stdout.on("data",R=>S.writeSync(I,R)),_.stderr.on("data",R=>S.writeSync(I,R));const lt=zt({input:_.stdout,crlfDelay:1/0});return lt.on("line",R=>{const dt=/^port (\d{1,5})$/.exec(R);if(!dt||f.port||f.stopped)return;const L=Number(dt[1]);if(L<1||L>65535){D("Build script printed an invalid port.");return}f.port=L,f.group.port=L,P(t),clearTimeout(f.deadline);let Y,Et=!1;const Ut=a(async()=>{if(!(Et||f.stopped||f.ready||_.exitCode!==null||_.signalCode!==null)){Et=!0;try{if(await d(L)){if(f.stopped||tt||_.exitCode!==null||_.signalCode!==null||(f.opening=he(f),await f.opening,f.stopped||tt||_.exitCode!==null||_.signalCode!==null))return;f.ready=!0,f.visited=o(),b.kind="web",K(t,e,{state:"ready",port:L,https:f.https}),clearInterval(f.poll),e==="main"&&await wt(t)}else o()>=Y&&D("Nothing answered on the build script\u2019s port within 60 seconds.")}finally{Et=!1}}},"check"),Kt=a(()=>{f.stopped||(Y=o()+6e4,f.poll=setInterval(()=>Ut().catch(ut=>D(ut.message)),250),Ut().catch(ut=>D(ut.message)))},"probeFrom");e==="main"&&p.get(t.dir)?.port===L?wt(t).then(Kt,ut=>D(ut.message)):Kt()}),f.deadline=setTimeout(()=>D("No port line within 60 minutes."),60*6e4),_.once("exit",()=>{mt(f)}),f.done=new Promise(R=>_.once("close",(dt,L)=>{clearTimeout(f.deadline),clearInterval(f.poll),lt.close(),S.closeSync(I),(async()=>{if(await mt(f),await me(f),le(f),W||await Mt(t,e,f.port),f.finished=!0,f.stopped)return;if(p.get(t.dir)===f){p.delete(t.dir),K(t,e,{serving:void 0});return}if(e==="main"&&!f.ready&&p.has(t.dir)&&!W){await ye(f);return}const Y=e===U&&!f.port?new Error('Build script exited without printing "port <n>".'):null;tt||N||dt!==0||f.port&&!f.ready||Y?await Bt(f,dt??1,L,tt||N||(f.port&&!f.ready?new Error("Build script exited before its port answered."):Y)):f.ready?(e==="main"?(delete b.main.state,delete b.main.port,delete b.main.https):Ct(b,e),P(t)):(b.kind="app",K(t,e,{state:"ready"}))})().catch(Y=>c(`build /${t.route} ${e}: ${Y.message}`)).finally(()=>{m.get(E)===f&&m.delete(E),R()})})),c(`build /${t.route} ${e}: started; log ${j}`),{status:"started"}}a(St,"launch");async function wt(t){const e=p.get(t.dir);e&&(p.delete(t.dir),K(t,"main",{serving:void 0}),await ct(e))}a(wt,"retire");async function ye(t){const{board:e,worktree:r}=t;await wt(e),c(`try /${e.route} main: the new serve exited before answering while the old one served; stopped the old serve and serving again in ${r}`);const{serving:s,...i}=t.metadata;try{await St(e,"main",r,i,`The new serve exited before answering while the old one served; stopped the old serve and serving again.
6
+ `)}catch(n){Rt(e,i,n)}}a(ye,"handOverFailed");function Rt(t,e,r){r.status!==503&&(C(t).state.main={...e,state:"failed"},It(t,"main",r),P(t))}a(Rt,"mainFailed");async function ge(t,e,r){const s=`try /${t.route} main:`;if(!O.has(t.dir)){O.add(t.dir);try{const i=await Xe(r);for(const n of new Set(i.map(l=>l.pgid))){await At(n);for(const l of i.filter($=>$.pgid===n))c(`${s} stopped pid ${l.pid} (pgid ${n}) in the old main worktree ${r}`)}}catch(i){c(`${s} could not stop the processes in the old main worktree ${r}: ${i.message}`)}}if(gt(r))try{await M(e,"worktree","remove","--force",r);return}catch(i){try{S.rmSync(r,{recursive:!0,force:!0}),c(`${s} git could not remove the old main worktree ${r}; removed the folder: ${(i.stderr||i.message).trim().split(`
7
+ `).at(-1)}`),await M(e,"worktree","prune")}catch(n){c(`${s} could not remove the old main worktree ${r}: ${n.message}`)}}}a(ge,"removeLegacyMain");async function Ht(t,e,r){const s=B(t,"main");if(x.has(s))throw new A("A build is already running for main.",409);x.add(s);try{const i=m.get(s);i?.ready&&!i.ending?(m.delete(s),p.set(t.dir,i)):i&&await Z(i,!0);const n=p.get(t.dir);n&&(r={...r,serving:{sha:n.sha,at:n.at,https:n.https}});const l=a(b=>T.join(Jt.homedir(),".claude","kickflip","try",`${t.route}-main${b}`),"worktreeOf"),$=l(n?.worktree===l("-a")?"-b":"-a");return S.mkdirSync(T.dirname($),{recursive:!0}),await ge(t,e,l("")),gt($)?await M($,"checkout","--detach","--force",r.sha):(await M(e,"worktree","prune"),await M(e,"worktree","add","--detach",$,r.sha)),await St(t,"main",$,r)}catch(i){throw Rt(t,r,i),i}finally{x.delete(s)}}a(Ht,"launchMain");async function Ot(t,{manifest:e=Q()}={}){if(await at(e),kt(e),W)throw new A("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(w=>!["route","key"].includes(w))||typeof t.route!="string"||typeof t.key!="string")throw new A("Send only the board route and card key.");const{route:r,key:s}=t;if(H(s)&&(!/^[\w.-]+\/[\w.-]+$/.test(s)||s.split("/").some(w=>w==="."||w==="..")))throw new A("Card key must be <file>/<id>.");const i=J(e).find(w=>w.route===r);if(!i)throw new A(`/${r} is not a board.`,404);if(H(s)&&!i.files.some(({slug:w,itemFile:v})=>v.items.some(j=>`${w}/${j.id}`===s)))throw new A("There is no item with this card key.",404);const n=await y();if(n)throw new A(n,409);const l=B(i,s),$=m.get(l);if(x.has(l)||$&&!$.ready&&!$.finished)throw new A(`A build is already running for ${s==="main"?"main":s===U?"the prototype site":"this card"}.`,409);if(s==="main"){const w=C(i).state.main;if(w?.state==="running")throw new A("A build is already running for main.",409);if(w?.state!=="failed"||!w.sha)throw new A("Main has no failed build to retry.",409);const{sha:v,branch:j,slug:I,at:_,script:E}=w;return Ht(i,et(r,e).repo,{sha:v,branch:j,slug:I,at:_,script:E,...i.build_target?{target:i.build_target}:{}})}x.add(l);const b=z.delete(l);try{m.has(l)&&await Z(m.get(l),!0);const w=s===U?Wt(i):await ae(i,s,e);return await St(i,s,w)}catch(w){throw it.delete(l),b&&w.status!==503&&K(i,s,{state:void 0,sha:void 0}),w}finally{x.delete(l)}}a(Ot,"start");const Wt=a(t=>{const e=Be(t.route);return gt(e)&&S.statSync(e).isDirectory()?S.realpathSync(e):""},"baseWorktree");async function $e(t,e){const{state:r}=C(t),s=B(t,U),i=m.get(s),n=(await Fe(t.dir)).length>0&&Wt(t);if(!n){i&&await Z(i),r.prototypes&&(r.prototypes=null,P(t));return}if(i?.ready&&!i.stopped&&!x.has(s)){const l=await M(n,"rev-parse","HEAD");if(l===r.prototypes?.sha)return;c(`try /${t.route} prototypes: the base moved to ${l.slice(0,7)}; restarting its serve`)}else if(i||x.has(s)||r.prototypes?.state==="failed"||await y())return;try{await Ot({route:t.route,key:U},{manifest:e})}catch(l){if([409,503].includes(l.status))return;throw r.prototypes={state:"failed"},It(t,U,l),P(t),l}}a($e,"serveBase");function Gt(t,e,{manifest:r=Q()}={}){const s=J(r).find(n=>n.route===t);if(!s)return null;const i=[...m.values()].find(n=>n.board.dir===s.dir&&n.key.split("/").at(-1).toLowerCase()===e.toLowerCase()&&n.ready&&!n.stopped&&n.child.exitCode===null&&n.child.signalCode===null);return i?{key:i.key,port:i.port}:null}a(Gt,"lookup");async function xt({manifest:t=Q(),routes:e}={}){if(await at(t),kt(t),W)return G;if(G&&e){if(!X){const r={routes:new Set,manifest:t};r.done=G.catch(()=>{}).then(()=>(X=null,xt({manifest:r.manifest,routes:[...r.routes]}))),X=r}X.manifest=t;for(const r of e)X.routes.add(r);return X.done}return G||(G=(async()=>{await we();const r=J(t),s=ft(je,{}).prs||{};for(const[i,n]of[...p])r.some(l=>l.dir===i)||(p.delete(i),await ct(n));for(const i of[...m.values()])if(!r.some(n=>n.dir===i.board.dir)||H(i.key)&&i.ready&&o()-i.visited>=Ye){await Z(i);const{state:n}=C(i.board);Ct(n,i.key),P(i.board)}for(const i of r){if(W)return;if(e&&!e.includes(i.route))continue;const{state:n}=C(i),l=et(i.route,t),$=l.branch||"main",b=_e(l);for(const{slug:w,itemFile:v}of i.files)for(const j of v.items){const I=`${w}/${j.id}`,_=Re(j,i.github||v.repo).map(E=>s[We(E)]);if(!(j.lane==="done"||_.length&&_.every(E=>["MERGED","CLOSED"].includes(E?.state))))try{const E=B(i,I),f=await ae(i,I,t);if(f){const D=await M(f,"rev-parse","HEAD");let lt=0;try{lt=Number(await M(f,"rev-list","--count",`${b}..HEAD`))}catch{}Object.assign(n.cards[I]||={},{head:D,ahead:lt,script:gt(te(f,i.build_target))}),P(i)}else if(!z.has(E))continue;const N=n.cards[I];if((z.has(E)||i.auto_build&&N.script&&N.ahead>0&&(N.sha||bt.get(E))!==N.head&&N.state!=="running")&&!x.has(E))try{await Ot({route:i.route,key:I},{manifest:t})}catch(D){if([409,503].includes(D.status))continue;throw K(i,I,{sha:N.head}),await Bt({board:i,key:I,logFile:It(i,I,D)},1),D}}catch(E){c(`try /${i.route} ${I}: ${E.message}`)}}await $e(i,t).catch(w=>c(`try /${i.route} prototypes: ${w.message}`));try{if(!l.repo||await y())continue;const w=await M(l.repo,"rev-parse",b);let v=!1;try{await M(l.repo,"cat-file","-e",`${w}:${qt(i.build_target)}`),v=!0}catch{}if(!v){const N=m.get(B(i,"main"));N&&await Z(N),await wt(i),n.main=null,P(i);continue}const j=n.main;if(j?.state==="running"||j?.sha===w&&(j.target||null)===(i.build_target||null)&&(j.state==="failed"||j.state==="ready"&&(n.kind!=="web"||Gt(i.route,"main",{manifest:t}))))continue;const I=B(i,"main");if(x.has(I))continue;const _=await M(l.repo,"log","-1","--format=%cI",w);let E=i.github;if(!E)try{E=Vt(await M(l.repo,"remote","get-url","origin"))}catch{}const f={sha:w,branch:$,slug:E||"",at:_,script:v,...i.build_target?{target:i.build_target}:{}};await Ht(i,l.repo,f)}catch(w){c(`try /${i.route} main: ${w.message}`)}}})().finally(()=>{G=null}),G)}a(xt,"tick");function Lt(t,e){if(nt.has(t.dir))return nt.get(t.dir);const r=(async()=>{const s=et(t.route,e),i=s.branch||"main",{state:n}=C(t);n.pull={...n.pull,running:!0},P(t);let l,$;try{l=await(s.local?rr:er)(s.repo,i)}catch(b){$=b.message}if(l===null?delete n.pull:n.pull={at:ht(),...$?{error:$}:{}},P(t),l===null)return null;if($)return c(`pull /${t.route}: ${$}`),{commits:0,error:$};if(l.commits){c(`pull /${t.route}: ${i} ${s.local?"fast-forwarded":"reset"} ${l.commits} commit${l.commits===1?"":"s"} to ${l.to.slice(0,7)}`);try{await u(t,"main-pulled",{branch:i,...l})}catch(b){c(`pull /${t.route}: could not send main-pulled (${b.message})`)}}return{commits:l.commits}})().finally(()=>nt.delete(t.dir));return nt.set(t.dir,r),r}a(Lt,"pullMain");async function ve({manifest:t=Q(),routes:e}={}){await at(t);const r=[];for(const s of J(t)){if(W)break;if(!(e&&!e.includes(s.route)))try{if(!et(s.route,t).repo)continue;(await Lt(s,t))?.commits&&r.push(s.route)}catch(i){c(`pull /${s.route} failed: ${i.message}`)}}return r}a(ve,"pullMains");async function be(t,{manifest:e=Q()}={}){if(await at(e),W)throw new A("The hub is stopping. Try again after it restarts.",503);if(!t||Array.isArray(t)||typeof t!="object"||Object.keys(t).some(n=>n!=="route")||typeof t.route!="string")throw new A("Send only the board route.");const r=J(e).find(n=>n.route===t.route);if(!r)throw new A(`/${t.route} is not a board.`,404);const s=et(r.route,e),i=await Lt(r,e);if(!i)throw new A("This board\u2019s folder has no origin to pull from.",409);return(i.commits||!s.local)&&xt({manifest:e,routes:[r.route]}).catch(n=>c(`try /${r.route} main: ${n.message}`)),i}a(be,"pull");async function ke(){W=!0;const t=Promise.all([...m.values()].map(e=>e.ready?Z(e):ct(e)).concat([...p.values()].map(e=>ct(e))));await Promise.all([t,G,...nt.values()])}return a(ke,"stop"),{start:Ot,tick:xt,stop:ke,lookup:Gt,initialize:at,pull:be,pullMains:ve,endUnshared:pe}}a(tr,"createTryManager");const $t=1e4,vt=a(async(o,...d)=>(await rt("git",["-C",o,...d],{encoding:"utf8",timeout:$t,env:Ae()})).stdout,"originGit");async function er(o,d,{run:h=vt}={}){const c=a(async(...p)=>(await h(...p)).trim(),"git"),y=a(p=>c(o,"rev-parse","--verify","--quiet",`${p}^{commit}`).catch(()=>""),"tip"),u=`refs/remotes/origin/${d}`,k=await y(`refs/heads/${d}`)||await y(u);try{await c(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:${u}`),await c(o,"checkout","--quiet","--force","-B",d,u),await c(o,"clean","--quiet","-fd")}catch(p){throw new Error(`can\u2019t pull: ${p.killed?`git fetch timed out after ${$t/1e3} s`:jt(p.stderr,{repo:o}).error}`)}const g=await y(u),m=k&&k!==g?Number(await c(o,"rev-list","--count",`${k}..${g}`)):0;return{from:k||g,to:g,commits:m}}a(er,"resetBranch");async function rr(o,d,{run:h=vt}={}){const c=a(async(...u)=>(await h(...u)).trim(),"git"),y=a(u=>{throw new Error(`can\u2019t pull: ${u}`)},"refuse");try{await c(o,"config","--get","remote.origin.url")}catch{return null}try{await c(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${d}:refs/remotes/origin/${d}`)}catch(u){y(u.killed?`git fetch timed out after ${$t/1e3} s`:jt(u.stderr,{repo:o}).error)}try{const u=await c(o,"rev-parse","--verify",`refs/heads/${d}^{commit}`),k=await c(o,"rev-parse","--verify",`refs/remotes/origin/${d}^{commit}`),g=a((O,F)=>c(o,"merge-base","--is-ancestor",O,F).then(()=>!0,x=>{if(x.code===1)return!1;throw x}),"isAncestor");if(u===k||await g(k,u))return{from:u,to:u,commits:0};await g(u,k)||y(`${d} has split from origin`);const m=Number(await c(o,"rev-list","--count",`${u}..${k}`)),p=(await c(o,"worktree","list","--porcelain")).split(`
5
8
 
6
- `).map(S=>S.split(`
7
- `)).find(S=>S.includes(`branch refs/heads/${d}`))?.[0].slice(9);return p?(await l(p,"status","--porcelain","--untracked-files=no")&&g("local changes"),await l(p,"merge","--ff-only","--quiet",k)):await l(o,"update-ref",`refs/heads/${d}`,k,f),{from:f,to:k,commits:m}}catch(f){if(f.message.startsWith("can\u2019t pull: "))throw f;g(String(f.stderr||f.message).trim().split(`
8
- `).at(-1))}}a(Qe,"pullBranch");async function yr({manifest:o=V(),log:d=console.log,routes:y}={}){const l=y?lt(Kt,{}):{};for(const g of z(o))if(!(y&&!y.includes(g.route)))try{const f=et(g.route,o);if(f.local||!f.repo)continue;const k=f.branch||"main",$=new Date().toISOString(),m=Date.now();try{await yt(f.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${k}:refs/remotes/origin/${k}`),l[g.route]={at:$,ok:!0}}catch(p){d(`fetch /${g.route} after ${Date.now()-m} ms: ${p.message}`);let S=g.github;if(!S&&!g.remote)try{S=Jt(await I(f.repo,"remote","get-url","origin"))}catch{}l[g.route]={at:$,ok:!1,...Et(p.killed?`git fetch timed out after ${wt/1e3} s`:p.stderr,{github:S||"",remote:g.remote,repo:f.repo})};continue}if(g.remote)try{await Ve(g,f.repo,k,d)}catch(p){d(`pushed branches /${g.route}: ${p.message}`)}}catch(f){d(`fetch /${g.route} failed: ${f.message}`)}Ut(Kt,l)}a(yr,"fetchTryBranches");async function Ve(o,d,y,l){const g=o.files.flatMap(({slug:$,itemFile:m})=>m.items.filter(p=>p.pushed?.branch&&!Pe(p)&&p.lane!=="done").map(p=>({key:`${$}/${p.id}`,branch:p.pushed.branch})));if(!g.length)return;const f=new Map((await yt(d,"ls-remote","--heads","origin")).split(`
9
- `).map($=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec($)).filter(Boolean).map($=>[$[2],$[1]])),k=Se(o.remote)?.host||"the remote";for(const $ of g){let m=f.get($.branch);if(!m)try{m=await I(d,"rev-parse","--verify","--quiet",`refs/heads/${$.branch}^{commit}`)}catch{}let p=!1;if(m)try{await I(d,"merge-base","--is-ancestor",m,`refs/remotes/origin/${y}`),p=!0}catch{}if(!(!p&&f.has($.branch)))try{await Ie(o.dir,$.key,{done:!0,force:!0,landed:p,closing:p?["branch-merged",{branch:$.branch,into:y,host:k}]:["branch-gone",{branch:$.branch,host:k}]})}catch(S){l(`pushed ${$.key}: ${S.message}`)}}}a(Ve,"settlePushed");const X=ze({waiting:a(()=>Ee.waiting("tailscale"),"waiting")}),gr=a((...o)=>X.start(...o),"startBuild"),$r=a((...o)=>X.tick(...o),"tryTick"),vr=a((...o)=>X.pull(...o),"pullNow"),br=a((...o)=>X.pullMains(...o),"pullMains"),kr=a(()=>X.stop(),"stopTries"),Sr=a(()=>X.endUnshared(),"endUnsharedTries"),Or=a((...o)=>X.initialize(...o),"initializeTries");export{_ as BuildError,q as TRY_PORTS,ze as createTryManager,Sr as endUnsharedTries,yr as fetchTryBranches,Or as initializeTries,re as installWorktree,Tt as isTryPort,Qe as pullBranch,br as pullMains,vr as pullNow,Ye as resetBranch,gr as startBuild,kr as stopTries,$r as tryTick};
9
+ `).map(O=>O.split(`
10
+ `)).find(O=>O.includes(`branch refs/heads/${d}`))?.[0].slice(9);return p?(await c(p,"status","--porcelain","--untracked-files=no")&&y("local changes"),await c(p,"merge","--ff-only","--quiet",k)):await c(o,"update-ref",`refs/heads/${d}`,k,u),{from:u,to:k,commits:m}}catch(u){if(u.message.startsWith("can\u2019t pull: "))throw u;y(String(u.stderr||u.message).trim().split(`
11
+ `).at(-1))}}a(rr,"pullBranch");async function Or({manifest:o=Q(),log:d=console.log,routes:h}={}){const c=h?ft(Qt,{}):{};for(const y of J(o))if(!(h&&!h.includes(y.route)))try{const u=et(y.route,o);if(u.local||!u.repo)continue;const k=u.branch||"main",g=new Date().toISOString(),m=Date.now();try{await vt(u.repo,"fetch","--quiet","--no-tags","origin",`+refs/heads/${k}:refs/remotes/origin/${k}`),c[y.route]={at:g,ok:!0}}catch(p){d(`fetch /${y.route} after ${Date.now()-m} ms: ${p.message}`);let O=y.github;if(!O&&!y.remote)try{O=Vt(await M(u.repo,"remote","get-url","origin"))}catch{}c[y.route]={at:g,ok:!1,...jt(p.killed?`git fetch timed out after ${$t/1e3} s`:p.stderr,{github:O||"",remote:y.remote,repo:u.repo})};continue}if(y.remote)try{await ir(y,u.repo,k,d)}catch(p){d(`pushed branches /${y.route}: ${p.message}`)}}catch(u){d(`fetch /${y.route} failed: ${u.message}`)}Yt(Qt,c)}a(Or,"fetchTryBranches");async function ir(o,d,h,c){const y=o.files.flatMap(({slug:g,itemFile:m})=>m.items.filter(p=>p.pushed?.branch&&!He(p)&&p.lane!=="done").map(p=>({key:`${g}/${p.id}`,branch:p.pushed.branch})));if(!y.length)return;const u=new Map((await vt(d,"ls-remote","--heads","origin")).split(`
12
+ `).map(g=>/^([\da-f]+)\trefs\/heads\/(.+)$/.exec(g)).filter(Boolean).map(g=>[g[2],g[1]])),k=Ie(o.remote)?.host||"the remote";for(const g of y){let m=u.get(g.branch);if(!m)try{m=await M(d,"rev-parse","--verify","--quiet",`refs/heads/${g.branch}^{commit}`)}catch{}let p=!1;if(m)try{await M(d,"merge-base","--is-ancestor",m,`refs/remotes/origin/${h}`),p=!0}catch{}if(!(!p&&u.has(g.branch)))try{await De(o.dir,g.key,{done:!0,force:!0,landed:p,closing:p?["branch-merged",{branch:g.branch,into:h,host:k}]:["branch-gone",{branch:g.branch,host:k}]})}catch(O){c(`pushed ${g.key}: ${O.message}`)}}}a(ir,"settlePushed");const q=tr({waiting:a(()=>Me.waiting("tailscale"),"waiting")}),xr=a((...o)=>q.start(...o),"startBuild"),Er=a((...o)=>q.tick(...o),"tryTick"),Tr=a((...o)=>q.pull(...o),"pullNow"),jr=a((...o)=>q.pullMains(...o),"pullMains"),_r=a(()=>q.stop(),"stopTries"),Ir=a(()=>q.endUnshared(),"endUnsharedTries"),Ar=a((...o)=>q.initialize(...o),"initializeTries");export{A as BuildError,V as TRY_PORTS,tr as createTryManager,Ir as endUnsharedTries,Or as fetchTryBranches,Ar as initializeTries,ae as installWorktree,_t as isTryPort,rr as pullBranch,jr as pullMains,Tr as pullNow,er as resetBranch,xr as startBuild,_r as stopTries,Er as tryTick};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovek4d/kickflip",
3
- "version": "0.7.4",
3
+ "version": "0.7.7",
4
4
  "description": "Follow and steer Claude Code sessions from your phone.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {