@lovek4d/kickflip 0.5.3 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CONTEXT.md +82 -46
  2. package/SKILL.md +21 -15
  3. package/bin/doctor.mjs +5 -4
  4. package/bin/install.mjs +3 -3
  5. package/bin/kickflip.mjs +12 -12
  6. package/bin/wait-events.mjs +2 -2
  7. package/board/agents.mjs +2 -2
  8. package/board/arch-review.mjs +4 -4
  9. package/board/bin/board.mjs +179 -92
  10. package/board/build-script.mjs +2 -1
  11. package/board/domain.mjs +9 -7
  12. package/board/finish.mjs +4 -4
  13. package/board/headless.mjs +9 -5
  14. package/board/onboarding.mjs +11 -0
  15. package/board/prototype-base.mjs +1 -0
  16. package/board/prototype-page.mjs +1 -0
  17. package/board/public/app-arch.js +1 -1
  18. package/board/public/app-cards.js +40 -47
  19. package/board/public/app-header.js +6 -7
  20. package/board/public/app-lanes.js +19 -45
  21. package/board/public/app-needs.js +36 -31
  22. package/board/public/app-snapshot.js +43 -70
  23. package/board/public/app-threads.js +42 -46
  24. package/board/public/app.html +9 -4
  25. package/board/public/app.js +78 -32
  26. package/board/public/hub-kit.js +62 -2
  27. package/board/public/hub.css +2 -0
  28. package/board/public/new-task.js +8 -6
  29. package/board/public/progress-review.html +46 -0
  30. package/board/public/progress-review.js +158 -0
  31. package/board/public/prototype-kit.js +109 -4
  32. package/board/public/prototype-switcher.js +130 -31
  33. package/board/public/prototype.html +57 -3
  34. package/board/public/router.js +7 -6
  35. package/board/public/shell.js +65 -24
  36. package/board/public/sidebar.css +6 -0
  37. package/board/public/sidebar.js +19 -69
  38. package/board/public/style.css +116 -73
  39. package/board/public/ui-cards.js +78 -57
  40. package/board/public/ui-enums.js +2 -2
  41. package/board/public/ui-header.js +23 -16
  42. package/board/public/ui-hub.js +19 -5
  43. package/board/public/ui-links.js +7 -7
  44. package/board/public/ui-order.js +1 -1
  45. package/board/public/ui-page-header.js +19 -7
  46. package/board/public/ui-patch.js +7 -33
  47. package/board/public/ui-progress-bar.js +28 -0
  48. package/board/public/ui-questions.js +25 -17
  49. package/board/public/ui-sidebar.js +31 -23
  50. package/board/public/ui-snapshot.js +44 -53
  51. package/board/public/ui-threads.js +57 -28
  52. package/board/public/ui.js +94 -18
  53. package/board/public/view-hub-settings.js +12 -58
  54. package/board/public/view-hub.js +5 -2
  55. package/board/public/view-notes.js +27 -7
  56. package/board/public/view-questions.js +90 -68
  57. package/board/public/view-settings.js +133 -15
  58. package/board/render-check.mjs +10 -0
  59. package/board/review.mjs +1 -1
  60. package/board/server.mjs +20 -19
  61. package/board/stop.mjs +1 -0
  62. package/board/store.mjs +29 -18
  63. package/board/sync.mjs +2 -0
  64. package/board/tickets.mjs +3 -3
  65. package/board/transcript.mjs +2 -2
  66. package/board/worktree.mjs +22 -21
  67. package/hooks-plugin/hooks/board-data-gate.mjs +9 -6
  68. package/hooks-plugin/hooks/usage-gate.mjs +20 -3
  69. package/hub/board-repo.mjs +1 -1
  70. package/hub/boards.mjs +4 -4
  71. package/hub/front-door.mjs +2 -2
  72. package/hub/github.mjs +3 -3
  73. package/hub/hub.mjs +11 -11
  74. package/hub/incoming.mjs +2 -2
  75. package/hub/owner-checks.mjs +1 -1
  76. package/hub/pr-report.mjs +2 -2
  77. package/hub/review-send.mjs +1 -1
  78. package/hub/sessions.mjs +2 -2
  79. package/hub/share.mjs +1 -1
  80. package/hub/slack.mjs +2 -2
  81. package/hub/try.mjs +6 -6
  82. package/hub/upgrade.mjs +1 -1
  83. package/hub/usage.mjs +3 -3
  84. package/hub/views.mjs +1 -1
  85. package/package.json +1 -1
  86. package/shared/card-prs.mjs +1 -1
  87. package/shared/machine-gate.mjs +2 -0
  88. package/shared/prototype-frame.js +1 -0
  89. package/shared/tailscale.mjs +1 -1
  90. package/shared/usage-gate.mjs +1 -1
  91. package/board/public/ui-phase-bars.js +0 -17
package/board/store.mjs CHANGED
@@ -1,23 +1,34 @@
1
- var Hi=Object.defineProperty;var r=(e,t)=>Hi(e,"name",{value:t,configurable:!0});import N from"node:fs/promises";import{constants as Ji,existsSync as De}from"node:fs";import h from"node:path";import Ot from"node:os";import{execFile as Pt}from"node:child_process";import{fileURLToPath as zi}from"node:url";import{BUILD_REFUSED as Dt,BUILDER_ASK as Xe,ITEM_STATUSES as fn,MAX_IMPLEMENTERS as Gi,MAX_PLANNERS as Qi,lineupPhases as At,mergeBlockers as Zi,modelName as et,noTakePicked as Vi,ownerQuestion as Ae,plans as Re,PLANNER_WORD as Yi,providerOf as Be,owesNext as Rt,pushTag as Xi,refOf as tt,roundOf as Nt,roundSettledBy as ea}from"./domain.mjs";import{GITHUB_STATE as pn,appendEvent as pe,listBoards as we,samePath as ve}from"../hub/boards.mjs";import{readUsageGate as wn,pauseReason as ta,pauseReasonFor as na,heldFor as ia,builderPick as mn}from"../shared/usage-gate.mjs";import{reviseQueued as aa,flushQueue as sa,queueStatus as oa,pushKindError as ra,readClaudeModel as ca}from"../hub/push.mjs";import{holdLine as da,CHECK_MINUTES as la}from"../hub/schedule.mjs";import{AT_RE as ua}from"../hub/duration.mjs";import{routeRecord as J}from"../hub/routes.mjs";import{ticketSource as Et,ticketIn as Lt,resolveTicket as fa,ticketTarget as pa,moveTicket as wa,titleTickets as ma,cardIdOf as ha}from"./tickets.mjs";import{LINEAR_NEEDS_KEY as ga,linearStatus as $a,linearWorks as ka}from"../hub/linear.mjs";import{validateBranch as ya}from"../hub/repos.mjs";import{withFileLock as nt}from"../hub/locks.mjs";import{audit as ba,auditLines as xa,boardAudit as hn,readAudit as va}from"../shared/audit.mjs";import{merge3 as Sa}from"./merge3.mjs";import{fetchPR as gn,reportOf as $n,githubLogin as _a,postToPR as ja,postReview as Oa,reviewsSince as Pa,CLAUDE_PREFIX as Te,CLAUDE_MARK as Ct}from"../hub/pr-report.mjs";import{allLanded as kn,cardPRs as Ne,hasPR as it,leastReady as Da,parsePRRef as yn,prKeyOf as Aa,prNames as qe,prURL as Ra,storedPRs as bn}from"../shared/card-prs.mjs";import{reviewLinks as at,topicItems as st,reviewersOf as Fe}from"./review.mjs";import{readiness as xn}from"../hub/readiness.mjs";import{slackDay as vn,savedSlack as Na,SlackError as Ea,SLACK_NEEDS_TOKEN as La}from"../hub/slack.mjs";import"../bin/install.mjs";import{readPermissions as Ca}from"./permissions.mjs";import{parseCheck as Ia,checkText as Ba}from"../shared/owner-check.mjs";import{RuleError as Ta,rulesOf as Sn,addRule as qa,editRule as Fa,removeRule as Ma,applyRules as Wa,readHubRules as ot,writeHubRules as Ua}from"../hub/rules.mjs";import{shareOf as Ka,validLogin as Ha}from"../hub/share.mjs";class Q extends Error{static{r(this,"BoardError")}}const u=r(e=>{throw new Q(e)},"fail"),Ja=r(()=>h.join(Ot.homedir(),".claude","kickflip","sites"),"sitesDir"),rt=r(()=>h.join(Ot.homedir(),".claude","waves"),"wavesDir"),_n=Symbol("read as"),$=r(async(e,t)=>{let i;try{i=await N.readFile(e,"utf8")}catch{return t}try{const n=JSON.parse(i);return n!==null&&typeof n=="object"&&!Array.isArray(n)?{...n,[_n]:i}:n}catch{return t}},"readJSON"),G=r(async e=>{try{return(await N.readFile(e,"utf8")).split(`
2
- `).filter(Boolean).map(t=>JSON.parse(t))}catch{return[]}},"readLines");async function ct(e,t){await N.mkdir(h.dirname(e),{recursive:!0});const i=`${e}.${process.pid}.tmp`;await N.writeFile(i,t),await N.rename(i,e)}r(ct,"replace");const y=r((e,t)=>{const{[_n]:i,...n}=t??{};return i===void 0?ct(e,JSON.stringify(t,null,2)+`
3
- `):nt(e,async()=>{const a=JSON.parse(i),s=JSON.parse(await N.readFile(e,"utf8").catch(()=>i));await ct(e,JSON.stringify(Sa(a,n,s),null,2)+`
4
- `)})},"writeJSON");async function Y(e,t){await N.mkdir(h.dirname(e),{recursive:!0}),await N.appendFile(e,JSON.stringify(t)+`
5
- `)}r(Y,"appendLine");async function Bo(e){/^[\w-]+$/.test(e||"")||u(`--site takes a route such as ios-orchestrator, not "${e}"`);const t=h.join(Ja(),e);return await N.stat(t).then(i=>i.isDirectory(),()=>!1)||u(`no site at ${t}`),t}r(Bo,"siteDir");const M=r(async e=>({title:"Board",items:"waves",...e?await $(h.join(e,"board.json"),{}):{}}),"boardConfig");async function q(e){const t=await M(e);if(t.items==="waves")try{return(await N.readdir(rt(),{withFileTypes:!0})).filter(n=>n.isDirectory()).map(n=>({slug:n.name,dir:h.join(rt(),n.name),file:h.join(rt(),n.name,"wave.json")}))}catch{return[]}const i=h.resolve(e,t.items);return[{slug:t.slug||h.basename(t.items,".json"),dir:h.dirname(i),file:i}]}r(q,"itemSources");const U=r((e,t)=>h.join(e,"data","threads",t.replace(/[^\w.-]+/g,"__")+".jsonl"),"threadFile"),L=r(e=>h.join(e,"data","owner.json"),"ownerFile"),Me=r(e=>h.join(e,"data","staged-questions.json"),"stagedFile"),A=r(e=>e||u("this command needs --site <route>"),"needSite"),j=r((e,t)=>typeof e=="string"&&e.trim()?e.trim():u(`${t} must be non-empty text`),"text"),z=r((e,t)=>e===void 0?void 0:t(e),"optional"),za=/^prototype-[\w-]+\.html(?:[?#]\S*)?$/,jn=r((e,t)=>typeof e=="string"&&(za.test(e)||/^https?:\/\/\S+$/.test(e))?e:u(`${t} must be an http(s) URL or a site page such as prototype-<topic>.html?q=<id>`),"pageUrl"),dt=r((e,t)=>typeof e=="boolean"?e:u(`${t} must be true or false`),"bool"),me=r((e,t)=>typeof e=="string"&&/^[\w.-]+$/.test(e)?e:u(`${t} must use only letters, digits, "_", "." and "-"`),"domId"),Ee=r((e,t,i)=>{(!e||typeof e!="object"||Array.isArray(e))&&u(`${i} must be a JSON object`);const n=Object.keys(e).filter(a=>!t.includes(a));n.length&&u(`${i} has unknown field ${n.map(a=>`"${a}"`).join(", ")}; allowed: ${t.join(", ")}`)},"onlyKeys");function C(e){const t=/^([^/\s]+)\/([^/\s]+)$/.exec(e||"");return t?{wave:t[1],id:t[2]}:u(`expected <wave>/<id>, not "${e}"`)}r(C,"parseKey");async function Le(e,t){const i=(await q(e)).find(a=>a.slug===t)||u(`this board has no wave "${t}"`),n=await $(i.file,null);return(!n||!Array.isArray(n.items))&&u(`${i.file} is not a wave file with items[]`),{source:i,wave:n}}r(Le,"loadWave");async function v(e,t){const{wave:i,id:n}=C(t);let a=await Le(e,i);!a.wave.items.some(o=>o.id===n)&&await Ms(e,t)&&(a=await Le(e,i));const s=a.wave.items.find(o=>o.id===n)||u(`wave "${i}" has no item "${n}"`);return{...a,item:s}}r(v,"loadItem");async function To(e,t,i){A(e),me(t,"the event id");const n=(await G(h.join(e,"data","events.jsonl"))).find(f=>f.id===t);n||u(`no event "${t}" in this site's data/events.jsonl`);const a=h.join(e,"data","replies",`${t}.html`);await ct(a,j(i,"the reply"));const s=n.kind==="ask"?`ask:${t}`:n.kind==="item-message"&&typeof n.data?.key=="string"?n.data.key:null,o=s?await Dn(e,s,is(i)):[],c=n.kind==="wave-triage"?["lineup","wave"].filter(f=>typeof n.data?.[f]=="string").map(f=>`${f}:${n.data[f]}`):[],d=n.kind==="ask"?`item:ask:${t}`:c[0];return d&&o.push(...await ti(e,d,"Acted on: replied")),{file:a,lines:o}}r(To,"writeReply");const On=r(e=>h.join(e,"data","working.json"),"workingFile");async function Ga(e){return(await $(On(e),{})).events||{}}r(Ga,"readWorking");const qo=["ask","item-message","wave-triage","overnight-triage"];async function Fo(e,t){if(!t.length)return;const i=new Date().toISOString(),n=await Ga(e);await y(On(e),{events:{...n,...Object.fromEntries(t.map(a=>[a,J(n,a)||i]))}})}r(Fo,"markWorking");const It=r(e=>h.join(e,"data","push-state.json"),"pushStateFile");async function Qa(e,t){A(e);const i=Date.parse((await $(It(e),{})).last_push);if(Number.isNaN(i))return 0;const n=h.join(e,"data","replies"),a=(await N.readdir(n).catch(()=>[])).filter(o=>/\.(html|json)$/.test(o));return(await Promise.all(a.map(o=>N.stat(h.join(n,o)).then(c=>c.mtimeMs,()=>0)))).filter(o=>o>i&&o<=t.getTime()).length}r(Qa,"repliesSincePush");async function Za(e,t){return await y(It(A(e)),{last_push:t.toISOString()}),It(e)}r(Za,"recordPush");const Va=h.join(h.dirname(zi(import.meta.url)),"..","bin","kickflip.mjs");function Ya(){if(!process.env.KICKFLIP_PUSH_CMD)return[process.execPath,Va,"push"];try{const e=JSON.parse(process.env.KICKFLIP_PUSH_CMD);if(Array.isArray(e)&&e.length&&e.every(t=>typeof t=="string"))return e}catch{}throw new Q("KICKFLIP_PUSH_CMD must be a JSON array of strings")}r(Ya,"pushCommand");async function Mo(e,t){const i=ra(t.kind);i&&u(i),t.title===void 0&&u("push needs --title");const n=new Date,a=es(t.body,await Qa(e,n)),s={kind:t.kind,title:t.title,body:a||void 0,url:as(t.url),tag:t.tag},[o,...c]=Ya(),d=[...c,...Object.entries(s).flatMap(([l,w])=>w===void 0?[]:[`--${l}`,w]),...t.urgent?["--urgent"]:[],...t.now?["--now"]:[]],f=await new Promise(l=>{Pt(o,d,{encoding:"utf8"},(w,m,g)=>l({error:w,said:`${m||""}${g||""}`.split(`
6
- `).filter(b=>b.trim())}))}),p=f.error?typeof f.error.code=="number"?f.error.code:1:0;if(p!==0)throw Object.assign(new Q([...f.said,`kickflip push failed (${typeof f.error.code=="number"?`exit ${p}`:f.error.message}); the push was not recorded`].join(`
7
- `)),{exitCode:p});return await Za(e,n),{lines:f.said}}r(Mo,"sendPush");const Xa=/(?:^| · )(and \d+ repl(?:y|ies))$/,es=r((e,t)=>[e,t&&`and ${t} ${t===1?"reply":"replies"}`].filter(Boolean).join(" \xB7 "),"withReplies"),Pn=160;function ts(e){const t=String(e).split(`
8
- `).map(i=>i.replace(/\s+/g," ").trim()).find(Boolean)||"";return t.length>Pn?`${t.slice(0,Pn-1).trimEnd()}\u2026`:t}r(ts,"pushLine");const ns={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" "};function is(e){return e.replace(/<details[\s\S]*?<\/details>/gi,`
1
+ var Bi=Object.defineProperty;var r=(e,t)=>Bi(e,"name",{value:t,configurable:!0});import P from"node:fs/promises";import{constants as Ci,existsSync as de}from"node:fs";import g from"node:path";import tn from"node:os";import{execFile as ea}from"node:child_process";import{fileURLToPath as Ti}from"node:url";import{BUILD_REFUSED as ta,BUILDER_ASK as St,COMMANDS as Pe,LANDER as qi,LANES as jt,MOVE as na,handedBackAt as nn,intakeKeyOf as Fi,MAX_IMPLEMENTERS as Mi,MAX_PLANNERS as Wi,builds as tt,lineupPhases as an,mergeBlockers as Ui,modelName as Ot,noTakePicked as Hi,ownerQuestion as Ae,plans as Fe,QUESTION_REOPEN as zi,PLANNER_WORD as Ki,providerOf as nt,owesNext as on,pushTag as Ji,refOf as Me,roundOf as sn,roundSettledBy as Gi,HELPER as ne,VISUAL as ue,visualAgent as Qi}from"./domain.mjs";import{GITHUB_STATE as rn,appendEvent as se,listBoards as fe,samePath as he}from"../hub/boards.mjs";import{readUsageGate as aa,pauseReason as Zi,pauseReasonFor as Vi,heldFor as Yi,holdsWork as Xi,builderPick as cn,pauseEnd as eo}from"../shared/usage-gate.mjs";import{reviseQueued as to,pushKindError as no,readClaudeModel as ln}from"../hub/push.mjs";import{AT_RE as ao}from"../hub/duration.mjs";import{routeRecord as V}from"../hub/routes.mjs";import{ticketSource as dn,ticketIn as un,resolveTicket as io,ticketTarget as oo,moveTicket as so,titleTickets as ro,cardIdOf as co}from"./tickets.mjs";import{LINEAR_NEEDS_KEY as lo,linearStatus as uo,linearWorks as fo}from"../hub/linear.mjs";import{validateBranch as po}from"../hub/repos.mjs";import{withFileLock as at}from"../hub/locks.mjs";import{audit as wo,auditLines as mo,boardAudit as ia,readAudit as ho}from"../shared/audit.mjs";import{merge3 as go}from"./merge3.mjs";import{fetchPR as oa,reportOf as sa,githubLogin as $o,postToPR as ko,postReview as bo,reviewsSince as yo,CLAUDE_PREFIX as it,CLAUDE_MARK as fn}from"../hub/pr-report.mjs";import{allLanded as xo,cardPRs as De,hasPR as Re,parsePRRef as pn,prKeyOf as ot,prNames as We,prURL as wn,storedPRs as st}from"../shared/card-prs.mjs";import{reviewLinks as vo,topicItems as _o,reviewersOf as mn}from"./review.mjs";import{readiness as ra}from"../hub/readiness.mjs";import{slackDay as So}from"../hub/slack.mjs";import"../bin/install.mjs";import{readPermissions as jo}from"./permissions.mjs";import{validBuildTarget as ca}from"./build-script.mjs";import{parseCheck as Oo,checkText as Po}from"../shared/owner-check.mjs";import{RuleError as Ao,rulesOf as la,addRule as Do,editRule as Ro,removeRule as No,applyRules as Eo,readHubRules as Pt,writeHubRules as Io}from"../hub/rules.mjs";import{shareEntries as Lo,validLogin as Bo}from"../hub/share.mjs";class ae extends Error{static{r(this,"BoardError")}}const d=r(e=>{throw new ae(e)},"fail"),Co=r(()=>g.join(tn.homedir(),".claude","kickflip","sites"),"sitesDir"),At=r(()=>g.join(tn.homedir(),".claude","waves"),"wavesDir"),da=Symbol("read as"),$=r(async(e,t)=>{let n;try{n=await P.readFile(e,"utf8")}catch{return t}try{const a=JSON.parse(n);return a!==null&&typeof a=="object"&&!Array.isArray(a)?{...a,[da]:n}:a}catch{return t}},"readJSON"),K=r(async e=>{try{return(await P.readFile(e,"utf8")).split(`
2
+ `).filter(Boolean).map(t=>JSON.parse(t))}catch{return[]}},"readLines");async function rt(e,t){await P.mkdir(g.dirname(e),{recursive:!0});const n=`${e}.${process.pid}.tmp`;await P.writeFile(n,t),await P.rename(n,e)}r(rt,"replace");const y=r((e,t)=>{const{[da]:n,...a}=t??{};return n===void 0?rt(e,JSON.stringify(t,null,2)+`
3
+ `):at(e,async()=>{const i=JSON.parse(n),o=JSON.parse(await P.readFile(e,"utf8").catch(()=>n));await rt(e,JSON.stringify(go(i,a,o),null,2)+`
4
+ `)})},"writeJSON");async function Y(e,t){await P.mkdir(g.dirname(e),{recursive:!0}),await P.appendFile(e,JSON.stringify(t)+`
5
+ `)}r(Y,"appendLine");async function Hr(e){/^[\w-]+$/.test(e||"")||d(`--site takes a route such as ios-orchestrator, not "${e}"`);const t=g.join(Co(),e);return await P.stat(t).then(n=>n.isDirectory(),()=>!1)||d(`no site at ${t}`),t}r(Hr,"siteDir");const ee=r(async e=>({title:"Board",items:"waves",...e?await $(g.join(e,"board.json"),{}):{}}),"boardConfig");async function F(e){const t=await ee(e);if(t.items==="waves")try{return(await P.readdir(At(),{withFileTypes:!0})).filter(a=>a.isDirectory()).map(a=>({slug:a.name,dir:g.join(At(),a.name),file:g.join(At(),a.name,"wave.json")}))}catch{return[]}const n=g.resolve(e,t.items);return[{slug:t.slug||g.basename(t.items,".json"),dir:g.dirname(n),file:n}]}r(F,"itemSources");const W=r((e,t)=>g.join(e,"data","threads",t.replace(/[^\w.-]+/g,"__")+".jsonl"),"threadFile"),I=r(e=>g.join(e,"data","owner.json"),"ownerFile"),ct=r(e=>g.join(e,"data","staged-questions.json"),"stagedFile"),A=r(e=>e||d("this command needs --site <route>"),"needSite"),j=r((e,t)=>typeof e=="string"&&e.trim()?e.trim():d(`${t} must be non-empty text`),"text"),X=r((e,t)=>e===void 0?void 0:t(e),"optional"),To=/^prototype-[\w-]+\.html(?:[?#]\S*)?$/,qo=/^visuals\/[\w-]+\/[\w-]+\.html$/,ua=r((e,t)=>typeof e=="string"&&(To.test(e)||qo.test(e)||/^https?:\/\/\S+$/.test(e))?e:d(`${t} must be an http(s) URL or a site page such as prototype-<topic>.html?q=<id>`),"pageUrl"),Dt=r((e,t)=>typeof e=="boolean"?e:d(`${t} must be true or false`),"bool"),ge=r((e,t)=>typeof e=="string"&&/^[\w.-]+$/.test(e)?e:d(`${t} must use only letters, digits, "_", "." and "-"`),"domId"),Ue=r((e,t,n)=>{(!e||typeof e!="object"||Array.isArray(e))&&d(`${n} must be a JSON object`);const a=Object.keys(e).filter(i=>!t.includes(i));a.length&&d(`${n} has unknown field ${a.map(i=>`"${i}"`).join(", ")}; allowed: ${t.join(", ")}`)},"onlyKeys");function R(e){const t=/^([^/\s]+)\/([^/\s]+)$/.exec(e||"");return t?{wave:t[1],id:t[2]}:d(`expected <wave>/<id>, not "${e}"`)}r(R,"parseKey");async function Ne(e,t){const n=(await F(e)).find(i=>i.slug===t)||d(`this board has no wave "${t}"`),a=await $(n.file,null);return(!a||!Array.isArray(a.items))&&d(`${n.file} is not a wave file with items[]`),{source:n,wave:a}}r(Ne,"loadWave");async function x(e,t){const{wave:n,id:a}=R(t);let i=await Ne(e,n);!i.wave.items.some(s=>s.id===a)&&await Qs(e,t)&&(i=await Ne(e,n));const o=i.wave.items.find(s=>s.id===a)||d(`wave "${n}" has no item "${a}"`);return{...i,item:o}}r(x,"loadItem");async function zr(e,t,n){A(e),ge(t,"the event id");const a=(await K(g.join(e,"data","events.jsonl"))).find(u=>u.id===t);a||d(`no event "${t}" in this site's data/events.jsonl`);const i=g.join(e,"data","replies",`${t}.html`);await rt(i,j(n,"the reply"));const o=a.kind==="ask"?`ask:${t}`:a.kind==="item-message"&&typeof a.data?.key=="string"?a.data.key:null,s=o?await wa(e,o,Qo(n)):[],c=a.kind==="wave-triage"?["lineup","wave"].filter(u=>typeof a.data?.[u]=="string").map(u=>`${u}:${a.data[u]}`):[],l=a.kind==="ask"?`item:ask:${t}`:c[0];return l&&s.push(...await Ja(e,l,"Acted on: replied")),{file:i,lines:s}}r(zr,"writeReply");const fa=r(e=>g.join(e,"data","working.json"),"workingFile");async function Fo(e){return(await $(fa(e),{})).events||{}}r(Fo,"readWorking");const Kr=["ask","item-message","wave-triage","overnight-triage"];async function Jr(e,t){if(!t.length)return;const n=new Date().toISOString(),a=await Fo(e);await y(fa(e),{events:{...a,...Object.fromEntries(t.map(i=>[i,V(a,i)||n]))}})}r(Jr,"markWorking");const hn=r(e=>g.join(e,"data","push-state.json"),"pushStateFile");async function Mo(e,t){A(e);const n=Date.parse((await $(hn(e),{})).last_push);if(Number.isNaN(n))return 0;const a=g.join(e,"data","replies"),i=(await P.readdir(a).catch(()=>[])).filter(s=>/\.(html|json)$/.test(s));return(await Promise.all(i.map(s=>P.stat(g.join(a,s)).then(c=>c.mtimeMs,()=>0)))).filter(s=>s>n&&s<=t.getTime()).length}r(Mo,"repliesSincePush");async function Wo(e,t){return await y(hn(A(e)),{last_push:t.toISOString()}),hn(e)}r(Wo,"recordPush");const Uo=g.join(g.dirname(Ti(import.meta.url)),"..","bin","kickflip.mjs");function Ho(){if(!process.env.KICKFLIP_PUSH_CMD)return[process.execPath,Uo,"push"];try{const e=JSON.parse(process.env.KICKFLIP_PUSH_CMD);if(Array.isArray(e)&&e.length&&e.every(t=>typeof t=="string"))return e}catch{}throw new ae("KICKFLIP_PUSH_CMD must be a JSON array of strings")}r(Ho,"pushCommand");async function Gr(e,t){const n=no(t.kind);n&&d(n),t.title===void 0&&d("push needs --title");const a=new Date,i=Ko(t.body,await Mo(e,a)),o={kind:t.kind,title:t.title,body:i||void 0,url:Zo(t.url),tag:t.tag},[s,...c]=Ho(),l=[...c,...Object.entries(o).flatMap(([p,f])=>f===void 0?[]:[`--${p}`,f]),...t.urgent?["--urgent"]:[],...t.now?["--now"]:[]],u=await new Promise(p=>{ea(s,l,{encoding:"utf8"},(f,m,h)=>p({error:f,said:`${m||""}${h||""}`.split(`
6
+ `).filter(k=>k.trim())}))}),w=u.error?typeof u.error.code=="number"?u.error.code:1:0;if(w!==0)throw Object.assign(new ae([...u.said,`kickflip push failed (${typeof u.error.code=="number"?`exit ${w}`:u.error.message}); the push was not recorded`].join(`
7
+ `)),{exitCode:w});return await Wo(e,a),{lines:u.said}}r(Gr,"sendPush");const zo=/(?:^| · )(and \d+ repl(?:y|ies))$/,Ko=r((e,t)=>[e,t&&`and ${t} ${t===1?"reply":"replies"}`].filter(Boolean).join(" \xB7 "),"withReplies"),pa=160;function Jo(e){const t=String(e).split(`
8
+ `).map(n=>n.replace(/\s+/g," ").trim()).find(Boolean)||"";return t.length>pa?`${t.slice(0,pa-1).trimEnd()}\u2026`:t}r(Jo,"pushLine");const Go={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" "};function Qo(e){return e.replace(/<details[\s\S]*?<\/details>/gi,`
9
9
  `).replace(/<(?:br|\/?(?:p|div|li|ul|ol|h\d|blockquote|pre|tr|summary))\b[^>]*>/gi,`
10
- `).replace(/<[^>]*>/g,"").replace(/&(?:#(x[\da-f]{1,6}|\d{1,7})|(\w+));/gi,(t,i,n)=>{const a=i&&parseInt(i.replace(/^x/i,""),/^x/i.test(i)?16:10);return i?a<=1114111?String.fromCodePoint(a):t:J(ns,n.toLowerCase())??t})}r(is,"htmlText");function as(e){if(!e||!e.startsWith("/"))return e;const t=new URL(e,"http://board");return/^#item=[^&]+$/.test(t.hash)&&(t.hash=`${t.hash}&reply`),/\/[\w-]+\.html$/.test(t.pathname)&&!t.pathname.endsWith("/index.html")&&t.searchParams.set("from","push"),t.pathname+t.search+t.hash}r(as,"pushUrl");const ss=r((e,t)=>`/${h.basename(e)}/#item=${encodeURIComponent(t)}&reply`,"threadUrl");async function Dn(e,t,i){const n=ts(i);if(!n)return[];const a=Xi(t),s=await aa(a,o=>[n,Xa.exec(o)?.[1]].filter(Boolean).join(" \xB7 "),ss(e,t));return s===null?[]:[`queued push ${a} now reads: ${s}`]}r(Dn,"freshenPush");const An=r(e=>h.join(e,"data","hidden.json"),"hiddenFile"),os=r(async e=>new Set((await $(An(e),{events:[]})).events||[]),"readHidden");let Rn=Promise.resolve();function Wo(e,t,i){me(t,"the event id");const n=Rn.then(async()=>{const a=await os(e);i?a.add(t):a.delete(t),await y(An(e),{events:[...a]})});return Rn=n.catch(()=>{}),n}r(Wo,"setHidden");const Nn=r(e=>h.join(e,"data","read.json"),"readMarksFile"),rs=r(async e=>(await $(Nn(e),{})).cards||{},"readMarks");let En=Promise.resolve();function Uo(e,t,i={}){const n=En.then(async()=>{const a=await rs(e),s=new Date().toISOString(),o=r((d,f)=>String(f||"")>d?f:d,"later"),c=Object.fromEntries([...new Set(t)].sort().map(d=>[d,o(a[d]||s,i[d])]));JSON.stringify(c)!==JSON.stringify(a)&&await y(Nn(e),{cards:c})});return En=n.catch(()=>{}),n}r(Uo,"markRead");const Bt=20*1024*1024,Se=r(e=>h.join(e,"data","uploads"),"uploadsDir"),Ce=r((e,t)=>h.join(Se(e),".meta",`${t}.json`),"uploadMeta"),Ln=/^\d+-[\w.-]+$/,Cn=/^[\w.+-]+\/[\w.+-]+$/,In=r(()=>Object.assign(new Q(`a file can be at most ${Bt/1024/1024} MB`),{status:413}),"tooLarge");async function Ko(e,t,{name:i,type:n,length:a}){if(A(e),a>Bt)throw In();const s=String(i||"").trim().slice(0,200)||"file",o=h.basename(s).replace(/[^\w.-]+/g,"-").replace(/^[.-]+/,"").slice(-80)||"file",c=String(n||"").split(";")[0].trim().toLowerCase(),d=Cn.test(c)?c:"application/octet-stream";await N.mkdir(h.dirname(Ce(e,"x")),{recursive:!0});let f,p;for(let m=Date.now();!p;m++)f=`${m}-${o}`,p=await N.open(h.join(Se(e),f),"wx").catch(g=>g.code==="EEXIST"?null:Promise.reject(g));const l=h.join(Se(e),f);let w=0;try{for await(const m of t){if(w+=m.length,w>Bt)throw In();await p.write(m)}}catch(m){throw await p.close(),await N.rm(l,{force:!0}),m}return await p.close(),await y(Ce(e,f),{name:s,type:d,size:w,at:new Date().toISOString()}),{name:s,url:`uploads/${f}`,path:l,type:d,size:w}}r(Ko,"saveUpload");async function Ho(e,t){if(!Ln.test(t||""))return null;const i=h.join(Se(e),t),n=await N.stat(i).catch(()=>null);if(!n?.isFile())return null;const a=await $(Ce(e,t),{});return{path:i,name:a.name||t,type:Cn.test(a.type||"")?a.type:"application/octet-stream",size:n.size}}r(Ho,"readUpload");const Bn=["github","slack"];async function re(e,t,{text:i,link:n,log:a,who:s="claude",via:o,kind:c}){A(e),String(t).startsWith("ask:")?await Je(e,t.slice(4),"the ask"):await v(e,t),/^[^\n]{1,40}$/.test(s.trim())||u("--who takes claude, love or a reviewer's name"),o!==void 0&&!Bn.includes(o)&&u(`--via takes ${Bn.join(" or ")}`);const d={ts:new Date().toISOString(),who:s.trim(),text:j(i,"--text"),link:z(n,p=>jn(p,"--link")),log:a,via:o,kind:c},f=U(e,t);return await Y(f,d),{file:f,lines:d.who==="claude"&&!o?await Dn(e,t,d.text):[]}}r(re,"appendThread");const he=r(e=>h.join(e,"data","ask-links.json"),"askLinksFile");async function _e(e){return(await $(he(e),{})).links||{}}r(_e,"readAskLinks");async function X(e,t){const{tickets:i}=ge(await M(e)),n=await q(e),a=/^ask:(.+)$/.exec(t)?.[1];if(a===void 0){const o=/^([^/\s]+)\/([^/\s]+)$/.exec(t);if(!o)return t;const c=n.find(p=>p.slug===o[1]),d=c&&await $(c.file,null);if(!d)return o[2];const f=(d.items||[]).find(p=>p.id===o[2]);return tt(d,f||{id:o[2]},i)}const s=e?J(await _e(e),a):void 0;if(s)return X(e,s);for(const o of n){const c=await $(o.file,null),d=(Array.isArray(c?.items)?c.items:[]).find(f=>f.from_ask===a);if(d)return tt(c,d,i)}return`task ${a}`}r(X,"cardName");async function Jo(e,t){const i=(await G(h.join(e,"data","events.jsonl"))).find(n=>n.id===t);return i?.kind==="ask"?X(e,`ask:${t}`):typeof i?.data?.key=="string"?X(e,i.data.key):`the ${i?.kind} event`}r(Jo,"eventCard");async function zo(e,t,i,n){A(e),me(t,"the event id"),(await G(h.join(e,"data","events.jsonl"))).some(c=>c.id===t&&c.kind==="ask")||u(`no ask "${t}" in this site's data/events.jsonl`),await Tt(e,t),await v(e,i);const s={ts:new Date().toISOString(),who:"claude",text:j(n,"--summary")},o=await _e(e);return await y(he(e),{links:{...o,[t]:i}}),await Y(U(e,i),s),he(e)}r(zo,"relateAsk");async function Go(e,t){await Je(e,t,"the ask");const i=await _e(e);return J(i,t)===void 0&&u(`ask "${t}" is not folded into a card`),delete i[t],await y(he(e),{links:i}),he(e)}r(Go,"unfoldAsk");const lt=r(e=>h.join(e,"data","ask-progress.json"),"askProgressFile");async function ut(e){return(await $(lt(e),{})).asks||{}}r(ut,"readAskProgress");async function Tt(e,t){const i=J(await ut(e),t)?.moved;i&&u(`ask "${t}" was moved to /${i.route}; it lives there now`)}r(Tt,"refuseMoved");async function Qo(e,t,i,n){await Je(e,t,"the ask"),await Tt(e,t);const a=await ut(e);return await y(lt(e),{asks:{...a,[t]:{state:i,at:new Date().toISOString(),...n===void 0?{}:{next:j(n,"--next")}}}}),lt(e)}r(Qo,"markAsk");const Tn=[void 0,"waiting"];async function cs(e,t,i){const n=`ask "${t}"`;if(i.some(p=>p.kind==="ask-withdrawn"&&p.data?.id===t&&!p.data?.key))return{why:`${n} was deleted by the owner`};const a=J(await ut(e),t);if(a?.moved)return{why:`${n} was already moved to /${a.moved.route}`};const s=J(await _e(e),t);if(s)return{why:`${n} is folded into ${s}; run ask unfold ${t} first`};const o=[];for(const p of await q(e)){const l=await $(p.file,null);for(const w of Array.isArray(l?.items)?l.items:[])w.from_ask===t&&o.push({key:`${p.slug}/${w.id}`,it:w})}const c=o.find(({it:p})=>it(p)||!Tn.includes(p.status));return c?{why:`${n} became ${c.key}, which has started`}:o.length?{items:o.map(p=>p.key)}:(await Promise.all(["html","json"].map(p=>N.stat(h.join(e,"data","replies",`${t}.${p}`)).then(()=>!0,()=>!1)))).some(Boolean)||!!a||i.some(p=>p.kind==="ask-done"&&p.data?.id===t)||(await G(U(e,`ask:${t}`))).some(p=>(p.who||"claude")==="claude"&&p.kind!=="status")?{why:`${n} is not New: Claude already answered it`}:null}r(cs,"askNotNew");async function ds(e,t,i){const n=/^uploads\/([\w.-]+)$/.exec(i?.url||"")?.[1],a=Ln.test(n||"")?h.join(Se(e),n):null;if(!a||!await N.stat(a).then(d=>d.isFile(),()=>!1))return{name:String(i?.name||n||"file")};await N.mkdir(h.dirname(Ce(t,"x")),{recursive:!0});const s=n.replace(/^\d+-/,"");let o;for(let d=Date.now();o=`${d}-${s}`,!await N.copyFile(a,h.join(Se(t),o),Ji.COPYFILE_EXCL).then(()=>!0,p=>p.code==="EEXIST"?!1:Promise.reject(p));d++);const c=await $(Ce(e,n),null);return c&&await y(Ce(t,o),c),{...i,url:`uploads/${o}`,path:h.join(Se(t),o)}}r(ds,"copyUpload");async function Zo(e,t,i){A(e),me(t,"the event id"),(typeof i!="string"||!i.trim())&&u("ask move needs --to <route>, the board to move it to");const n=await G(h.join(e,"data","events.jsonl")),a=n.find(S=>S.id===t&&S.kind==="ask")||u(`no ask "${t}" in this site's data/events.jsonl`),s=we(),o=s.find(S=>S.route===i)||u(`--to ${i}: no board at /${i} in the hub's manifest; the boards are ${s.map(S=>S.route).join(", ")||"none"}`);ve(o.dir,e)&&u(`--to ${i}: that is this board`);const c=await cs(e,t,n);c?.why&&u(c.why);const d=n.filter(S=>S.kind==="ask-edited"&&S.data?.id===t&&typeof S.data?.text=="string").sort((S,_)=>String(S.ts).localeCompare(String(_.ts))),f=d.at(-1),p=[];for(const S of[...a.data?.files||[],...d.flatMap(_=>_.data.files||[])])p.push(await ds(e,o.dir,S));const l={route:h.basename(e),title:(await M(e)).title,id:t},w=f?f.data.text:a.data?.text||"",m=await pe(o,"ask",{text:w,...p.length?{files:p}:{},from:l}),g={route:i,title:o.title,id:m},b=new Date().toISOString(),P=await ut(e);await y(lt(e),{asks:{...P,[t]:{state:"done",at:b,moved:g}}});const D=`/${i}/#item=${encodeURIComponent(`ask:${m}`)}`;await Y(U(e,`ask:${t}`),{ts:b,who:"claude",text:`Moved to ${o.title}.`,link:D});for(const S of c?.items||[])await se(e,S,{status:"dropped",force:!0,text:`Dropped: its ask moved to ${o.title}.`});return{file:h.join(o.dir,"data","events.jsonl"),lines:[`moved to ${D}`,...(c?.items||[]).map(S=>`dropped: ${S}`)]}}r(Zo,"moveAsk");const qn=r(e=>h.join(e,"data","checked.json"),"checkedFile");async function Vo(e,t,{idle:i}={}){A(e),i&&t!==void 0&&u("pass --every or --idle, not both"),i===void 0&&t===void 0&&(i=!await ls(e));const n=new Date,a=i?null:t===void 0?la.away:/^[1-9]\d{0,3}$/.test(String(t))?Number(t):u("--every takes the minutes between checks, such as 30");return await y(qn(e),i?{at:n.toISOString(),every:null,idle:!0}:{at:n.toISOString(),every:a}),{file:qn(e),every:a,lines:await us()}}r(Vo,"writeChecked");async function Yo(e,t,{state:i,text:n}={}){return A(e),["posted","needs"].includes(i)||u("review record needs --state posted or --state needs"),String(n??"").trim()||u("review record needs the review text, as --text or --file"),Mn(e,await Fn(e,t,"review record"),{state:i,text:n})}r(Yo,"recordReview");async function Fn(e,t,i){/^[1-9]\d{0,6}$/.test(String(t))||u(`${i} takes the PR number, such as 437, not "${t}"`);const n=await $(h.join(e,"data","incoming.json"),{});return{...(n?.prs||[]).find(o=>o.number===Number(t))||(await $(h.join(e,"data","reviews.json"),{}))[t]||u(`PR #${t} is not an incoming PR on this board`),repo:n?.repo||ee(e,{})}}r(Fn,"incomingPR");async function Mn(e,{number:t,title:i,author:n,url:a,touches:s=[],asked:o=!1,head:c},{state:d,text:f,review_url:p}){const l=h.join(e,"data","reviews.json"),w=await $(l,{});return w[t]={number:Number(t),title:i,author:n,url:a,touches:s,asked:o,head:c,state:d,text:f,...p?{review_url:p}:{},at:new Date().toISOString()},await y(l,w),{file:l,lines:[]}}r(Mn,"saveReview");async function Xo(e,t,{words:i,readAt:n}){A(e);const a=await Fn(e,t,"review post"),s=String(i??"").trim()||u("review post needs the review text, as --file"),o=`${Te}Claude review from the ${(await M(e)).title} board`;(s.startsWith(Te.trim())||s.includes("Claude review from")||s.includes(Ct))&&u(`review post adds the ${Te.trim()} header and the claude-bridge marker itself; pass only your words`);const c=a.repo||u("review post needs a board on GitHub; this board has no repo there");Number.isNaN(Date.parse(n))&&u("--read-at takes the time you read the PR, such as 2026-11-05T09:30:00Z");const d=ge(await M(e)).github_login;try{const f=await Pa({repo:c,pr:a.number,since:n});f.length&&u(`PR #${t} got ${f.length===1?"a review":`${f.length} reviews`} after ${n} (${f.map(l=>`${l.login} at ${l.at}`).join(", ")}); reread the PR and review again`);const p=await Oa({repo:c,pr:a.number,head:a.head,body:`${o}
10
+ `).replace(/<[^>]*>/g,"").replace(/&(?:#(x[\da-f]{1,6}|\d{1,7})|(\w+));/gi,(t,n,a)=>{const i=n&&parseInt(n.replace(/^x/i,""),/^x/i.test(n)?16:10);return n?i<=1114111?String.fromCodePoint(i):t:V(Go,a.toLowerCase())??t})}r(Qo,"htmlText");function Zo(e){if(!e||!e.startsWith("/"))return e;const t=new URL(e,"http://board");return/^#item=[^&]+$/.test(t.hash)&&(t.hash=`${t.hash}&reply`),/\/[\w-]+\.html$/.test(t.pathname)&&!t.pathname.endsWith("/index.html")&&t.searchParams.set("from","push"),t.pathname+t.search+t.hash}r(Zo,"pushUrl");const Vo=r((e,t)=>`/${g.basename(e)}/#item=${encodeURIComponent(t)}&reply`,"threadUrl");async function wa(e,t,n){const a=Jo(n);if(!a)return[];const i=Ji(t),o=await to(i,s=>[a,zo.exec(s)?.[1]].filter(Boolean).join(" \xB7 "),Vo(e,t));return o===null?[]:[`queued push ${i} now reads: ${o}`]}r(wa,"freshenPush");const ma=r(e=>g.join(e,"data","hidden.json"),"hiddenFile"),Yo=r(async e=>new Set((await $(ma(e),{events:[]})).events||[]),"readHidden");let ha=Promise.resolve();function Qr(e,t,n){ge(t,"the event id");const a=ha.then(async()=>{const i=await Yo(e);n?i.add(t):i.delete(t),await y(ma(e),{events:[...i]})});return ha=a.catch(()=>{}),a}r(Qr,"setHidden");const ga=r(e=>g.join(e,"data","read.json"),"readMarksFile"),Xo=r(async e=>(await $(ga(e),{})).cards||{},"readMarks");let $a=Promise.resolve();function Zr(e,t,n={}){const a=$a.then(async()=>{const i=await Xo(e),o=new Date().toISOString(),s=r((l,u)=>String(u||"")>l?u:l,"later"),c=Object.fromEntries([...new Set(t)].sort().map(l=>[l,s(i[l]||o,n[l])]));JSON.stringify(c)!==JSON.stringify(i)&&await y(ga(e),{cards:c})});return $a=a.catch(()=>{}),a}r(Zr,"markRead");const gn=20*1024*1024,Ee=r(e=>g.join(e,"data","uploads"),"uploadsDir"),He=r((e,t)=>g.join(Ee(e),".meta",`${t}.json`),"uploadMeta"),ka=/^\d+-[\w.-]+$/,ba=/^[\w.+-]+\/[\w.+-]+$/,ya=r(()=>Object.assign(new ae(`a file can be at most ${gn/1024/1024} MB`),{status:413}),"tooLarge");async function Vr(e,t,{name:n,type:a,length:i}){if(A(e),i>gn)throw ya();const o=String(n||"").trim().slice(0,200)||"file",s=g.basename(o).replace(/[^\w.-]+/g,"-").replace(/^[.-]+/,"").slice(-80)||"file",c=String(a||"").split(";")[0].trim().toLowerCase(),l=ba.test(c)?c:"application/octet-stream";await P.mkdir(g.dirname(He(e,"x")),{recursive:!0});let u,w;for(let m=Date.now();!w;m++)u=`${m}-${s}`,w=await P.open(g.join(Ee(e),u),"wx").catch(h=>h.code==="EEXIST"?null:Promise.reject(h));const p=g.join(Ee(e),u);let f=0;try{for await(const m of t){if(f+=m.length,f>gn)throw ya();await w.write(m)}}catch(m){throw await w.close(),await P.rm(p,{force:!0}),m}return await w.close(),await y(He(e,u),{name:o,type:l,size:f,at:new Date().toISOString()}),{name:o,url:`uploads/${u}`,path:p,type:l,size:f}}r(Vr,"saveUpload");async function Yr(e,t){if(!ka.test(t||""))return null;const n=g.join(Ee(e),t),a=await P.stat(n).catch(()=>null);if(!a?.isFile())return null;const i=await $(He(e,t),{});return{path:n,name:i.name||t,type:ba.test(i.type||"")?i.type:"application/octet-stream",size:a.size}}r(Yr,"readUpload");const xa=["github","slack"];async function U(e,t,{text:n,link:a,log:i,who:o="claude",via:s,kind:c,by:l,command:u,card:w}){A(e),String(t).startsWith("ask:")?await pt(e,t.slice(4),"the ask"):await x(e,t),/^[^\n]{1,40}$/.test(o.trim())||d("--who takes claude, love or a reviewer's name"),s!==void 0&&!xa.includes(s)&&d(`--via takes ${xa.join(" or ")}`);const p={ts:new Date().toISOString(),who:o.trim(),text:j(n,"--text"),link:X(a,m=>ua(m,"--link")),log:i,via:s,kind:c,by:l,command:u,card:w},f=W(e,t);return await Y(f,p),{file:f,lines:p.who==="claude"&&!s?await wa(e,t,p.text):[]}}r(U,"appendThread");const _e=r(e=>g.join(e,"data","ask-links.json"),"askLinksFile");async function Ie(e){return(await $(_e(e),{})).links||{}}r(Ie,"readAskLinks");async function J(e,t){const{tickets:n}=$e(await ee(e)),a=await F(e),i=/^ask:(.+)$/.exec(t)?.[1];if(i===void 0){const s=/^([^/\s]+)\/([^/\s]+)$/.exec(t);if(!s)return t;const c=a.find(w=>w.slug===s[1]),l=c&&await $(c.file,null);if(!l)return s[2];const u=(l.items||[]).find(w=>w.id===s[2]);return Me(l,u||{id:s[2]},n)}const o=e?V(await Ie(e),i):void 0;if(o)return J(e,o);for(const s of a){const c=await $(s.file,null),l=(Array.isArray(c?.items)?c.items:[]).find(u=>u.from_ask===i);if(l)return Me(c,l,n)}return`task ${i}`}r(J,"cardName");async function Xr(e,t){const n=(await K(g.join(e,"data","events.jsonl"))).find(a=>a.id===t);return n?.kind==="ask"?J(e,`ask:${t}`):typeof n?.data?.key=="string"?J(e,n.data.key):`the ${n?.kind} event`}r(Xr,"eventCard");async function ec(e,t,n){A(e),ge(t,"the event id");const a=j(n,"--text"),i=(await K(g.join(e,"data","events.jsonl"))).find(s=>s.id===t)||d(`no event "${t}" in this site's data/events.jsonl`),o=Fi(i)||d(`event "${t}" isn't intake: a new task, the owner's message in an ask's thread, or a card an agent added`);return await se({dir:e},"intake-handoff",{event:t,key:o,text:a}),await U(e,o,{text:`Intake triage handed this to the orchestrator: ${a}`,kind:"status"}),{lines:[]}}r(ec,"intakeHandoff");async function tc(e,t,n,a){A(e),ge(t,"the event id"),(await K(g.join(e,"data","events.jsonl"))).some(c=>c.id===t&&c.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`),await $n(e,t),await x(e,n);const o={ts:new Date().toISOString(),who:"claude",text:j(a,"--summary")},s=await Ie(e);return await y(_e(e),{links:{...s,[t]:n}}),await Y(W(e,n),o),_e(e)}r(tc,"relateAsk");async function nc(e,t){await pt(e,t,"the ask");const n=await Ie(e);return V(n,t)===void 0&&d(`ask "${t}" is not folded into a card`),delete n[t],await y(_e(e),{links:n}),_e(e)}r(nc,"unfoldAsk");const Rt=r(e=>g.join(e,"data","ask-progress.json"),"askProgressFile");async function Nt(e){return(await $(Rt(e),{})).asks||{}}r(Nt,"readAskProgress");async function $n(e,t){const n=V(await Nt(e),t)?.moved;n&&d(`ask "${t}" was moved to /${n.route}; it lives there now`)}r($n,"refuseMoved");async function ac(e,t,n,a){await pt(e,t,"the ask"),await $n(e,t);const i=await Nt(e);return await y(Rt(e),{asks:{...i,[t]:{state:n,at:new Date().toISOString(),...a===void 0?{}:{next:j(a,"--next")}}}}),Rt(e)}r(ac,"markAsk");async function es(e,t,n){const a=`ask "${t}"`;if(n.some(w=>w.kind==="ask-withdrawn"&&w.data?.id===t&&!w.data?.key))return{why:`${a} was deleted by the owner`};const i=V(await Nt(e),t);if(i?.moved)return{why:`${a} was already moved to /${i.moved.route}`};const o=V(await Ie(e),t);if(o)return{why:`${a} is folded into ${o}; run ask unfold ${t} first`};const s=[];for(const w of await F(e)){const p=await $(w.file,null);for(const f of Array.isArray(p?.items)?p.items:[])f.from_ask===t&&s.push({key:`${w.slug}/${f.id}`,it:f})}const c=s.find(({it:w})=>Re(w)||Qe(w));return c?{why:`${a} became ${c.key}, which has started`}:s.length?{items:s.map(w=>w.key)}:(await Promise.all(["html","json"].map(w=>P.stat(g.join(e,"data","replies",`${t}.${w}`)).then(()=>!0,()=>!1)))).some(Boolean)||!!i||n.some(w=>w.kind==="ask-done"&&w.data?.id===t)||(await K(W(e,`ask:${t}`))).some(w=>(w.who||"claude")==="claude"&&w.kind!=="status")?{why:`${a} is not New: Claude already answered it`}:null}r(es,"askNotNew");async function ts(e,t,n){const a=/^uploads\/([\w.-]+)$/.exec(n?.url||"")?.[1],i=ka.test(a||"")?g.join(Ee(e),a):null;if(!i||!await P.stat(i).then(l=>l.isFile(),()=>!1))return{name:String(n?.name||a||"file")};await P.mkdir(g.dirname(He(t,"x")),{recursive:!0});const o=a.replace(/^\d+-/,"");let s;for(let l=Date.now();s=`${l}-${o}`,!await P.copyFile(i,g.join(Ee(t),s),Ci.COPYFILE_EXCL).then(()=>!0,w=>w.code==="EEXIST"?!1:Promise.reject(w));l++);const c=await $(He(e,a),null);return c&&await y(He(t,s),c),{...n,url:`uploads/${s}`,path:g.join(Ee(t),s)}}r(ts,"copyUpload");async function ic(e,t,n){A(e),ge(t,"the event id"),(typeof n!="string"||!n.trim())&&d("ask move needs --to <route>, the board to move it to");const a=await K(g.join(e,"data","events.jsonl")),i=a.find(O=>O.id===t&&O.kind==="ask")||d(`no ask "${t}" in this site's data/events.jsonl`),o=fe(),s=o.find(O=>O.route===n)||d(`--to ${n}: no board at /${n} in the hub's manifest; the boards are ${o.map(O=>O.route).join(", ")||"none"}`);he(s.dir,e)&&d(`--to ${n}: that is this board`);const c=await es(e,t,a);c?.why&&d(c.why);const l=a.filter(O=>O.kind==="ask-edited"&&O.data?.id===t&&typeof O.data?.text=="string").sort((O,B)=>String(O.ts).localeCompare(String(B.ts))),u=l.at(-1),w=[];for(const O of[...i.data?.files||[],...l.flatMap(B=>B.data.files||[])])w.push(await ts(e,s.dir,O));const p={route:g.basename(e),title:(await ee(e)).title,id:t},f=u?u.data.text:i.data?.text||"",m=await se(s,"ask",{text:f,...w.length?{files:w}:{},from:p}),h={route:n,title:s.title,id:m},k=new Date().toISOString(),v=await Nt(e);await y(Rt(e),{asks:{...v,[t]:{state:"done",at:k,moved:h}}});const E=`/${n}/#item=${encodeURIComponent(`ask:${m}`)}`;await Y(W(e,`ask:${t}`),{ts:k,who:"claude",text:`Moved to ${s.title}.`,link:E});for(const O of c?.items||[])await we(e,O,{done:!0,force:!0,text:`Closed: its ask moved to ${s.title}.`});return{file:g.join(s.dir,"data","events.jsonl"),lines:[`moved to ${E}`,...(c?.items||[]).map(O=>`closed: ${O}`)]}}r(ic,"moveAsk");async function oc(e,t,{state:n,text:a}={}){return A(e),["posted","needs"].includes(n)||d("review record needs --state posted or --state needs"),String(a??"").trim()||d("review record needs the review text, as --text or --file"),_a(e,await va(e,t,"review record"),{state:n,text:a})}r(oc,"recordReview");async function va(e,t,n){/^[1-9]\d{0,6}$/.test(String(t))||d(`${n} takes the PR number, such as 437, not "${t}"`);const a=await $(g.join(e,"data","incoming.json"),{});return{...(a?.prs||[]).find(s=>s.number===Number(t))||(await $(g.join(e,"data","reviews.json"),{}))[t]||d(`PR #${t} is not an incoming PR on this board`),repo:a?.repo||te(e,{})}}r(va,"incomingPR");async function _a(e,{number:t,title:n,author:a,url:i,touches:o=[],asked:s=!1,head:c},{state:l,text:u,review_url:w}){const p=g.join(e,"data","reviews.json"),f=await $(p,{});return f[t]={number:Number(t),title:n,author:a,url:i,touches:o,asked:s,head:c,state:l,text:u,...w?{review_url:w}:{},at:new Date().toISOString()},await y(p,f),{file:p,lines:[]}}r(_a,"saveReview");async function sc(e,t,{words:n,readAt:a}){A(e);const i=await va(e,t,"review post"),o=String(n??"").trim()||d("review post needs the review text, as --file"),s=`${it}Claude review from the ${(await ee(e)).title} board`;(o.startsWith(it.trim())||o.includes("Claude review from")||o.includes(fn))&&d(`review post adds the ${it.trim()} header and the claude-bridge marker itself; pass only your words`);const c=i.repo||d("review post needs a board on GitHub; this board has no repo there");Number.isNaN(Date.parse(a))&&d("--read-at takes the time you read the PR, such as 2026-11-05T09:30:00Z");const l=$e(await ee(e)).github_login;try{const u=await yo({repo:c,pr:i.number,since:a});u.length&&d(`PR #${t} got ${u.length===1?"a review":`${u.length} reviews`} after ${a} (${u.map(p=>`${p.login} at ${p.at}`).join(", ")}); reread the PR and review again`);const w=await bo({repo:c,pr:i.number,head:i.head,body:`${s}
11
11
 
12
- ${s}
12
+ ${o}
13
13
 
14
- ${Ct}`,login:d});return await Mn(e,a,{state:"posted",text:s,review_url:p}),{lines:[`posted review on PR #${t}: ${p}`]}}catch(f){if(f instanceof Q)throw f;u(f.message)}}r(Xo,"postIncomingReview");async function ls(e){for(const t of await q(e)){const i=await $(t.file,null);if((i?.items||[]).some(n=>!I.includes(n.status)&&Ne(n,ee(e,i)).length))return!0}return!1}r(ls,"hasOpenPR");async function us(){const e=oa(new Date);if(e.hold)return e.count?[`queued pushes held: ${da(e.hold)}`]:[];const t=await sa();return t?[`sent ${t.sent} of ${t.count} queued push${t.count===1?"":"es"}${t.ok?"":"; the rest stay queued"}`]:[]}r(us,"flushAfterTick");const ft=r(e=>h.join(e,"data","handshake.json"),"handshakeFile");async function Wn(e){const t=await $(ft(e),null);if(!t?.hello)return t;const{hello:i,...n}=t;return{...n,ping:n.ping||i}}r(Wn,"readHandshake");async function er(e,t){A(e),me(t,"the event id");const i=(await G(h.join(e,"data","events.jsonl"))).find(a=>a.id===t);i?.kind!=="ping"&&i?.kind!=="hello"&&u(`no ping event "${t}" in this site's data/events.jsonl`);const n=await Wn(e)||{};return n.ping?.id&&n.ping.id!==t&&u(`"${t}" is an older ping; ack the newest, "${n.ping.id}"`),await y(ft(e),{...n,ping:n.ping||{id:t,at:i.ts,session:null},ack:{id:t,at:new Date().toISOString()}}),ft(e)}r(er,"ackPing");async function tr(e){const t=await Wn(e);t&&!t.done_at&&await y(ft(e),{...t,done_at:new Date().toISOString()})}r(tr,"finishHandshake");const qt={name:/^[^\n]{1,40}$/,slack_user:/^U[A-Z0-9]+$/,dm:/^D[A-Z0-9]+$/,github:/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/},We=r((e,t)=>e.trim().toLowerCase()===t.trim().toLowerCase(),"sameName");function Ft(e,t){return Array.isArray(e)||u(`${t} must be a JSON array of reviewers`),e.forEach((i,n)=>{const a=`${t}[${n}]`;Ee(i,Object.keys(qt),a);for(const[s,o]of Object.entries(qt))(typeof i[s]!="string"||!o.test(i[s].trim()))&&u(`${a}.${s} must match ${o}`);e.slice(0,n).some(s=>We(s.name,i.name))&&u(`${t} names "${i.name}" twice`)}),e.map(i=>Object.fromEntries(Object.keys(qt).map(n=>[n,i[n].trim()])))}r(Ft,"checkReviewers");async function Mt(e,t){const{file:i,reviewers:n}=await pt(e,s=>{const o=Ft(t(s.reviewers||[]),"the reviewers");return{set:{reviewers:o},reviewers:o}}),a=[];for(const s of await q(e)){const o=await $(s.file,null);(o?.items||[]).some(c=>!I.includes(c.status))&&(await y(s.file,{...o,reviewers:n}),a.push(`wave ${s.slug}: reviewers copied`))}return{file:i,lines:a}}r(Mt,"writeReviewers");const nr=r((e,t)=>Mt(e,()=>Ft(t,"the reviewer file")),"setReviewers"),Un=r((e,t)=>((typeof e!="string"||!/^\d+(?:\.\d+)?$/.test(e)||Number(e)<=0)&&u("--ts must be a Slack message timestamp"),(typeof t!="string"||!/^D[A-Z0-9]+$/.test(t))&&u("--channel must be a DM channel id"),{ts:e,channel:t}),"linkMessage"),Wt=r((e,t,i)=>Fe(e,t).find(n=>n.name===i)||u(`no reviewer "${i}" on this wave or board`),"reviewerOn"),ir=r((e,t,i,{ts:n,channel:a})=>je(e,async()=>{const s=Un(n,a),{source:o,wave:c,item:d}=await v(e,t);return Wt(c,(await M(e)).reviewers,i),d.review_link_ts={...d.review_link_ts,[i]:s},await y(o.file,c),o.file}),"reviewSent"),ar=r((e,t,i,n)=>je(e,async()=>{(typeof n!="string"||!/^[\w+-]+$/.test(n))&&u("emoji must be a Slack emoji name without colons");const{source:a,wave:s,item:o}=await v(e,t),c=Wt(s,(await M(e)).reviewers,i),d=at(s,o,t,st())[i];d?.ts||u(`no review link recorded for ${i} on ${t}`);const f=J(o.review_link_ts,i),p=typeof f=="object"&&Array.isArray(f?.reactions)?f.reactions:[];return p.includes(n)||(o.review_link_ts={...o.review_link_ts,[i]:{...d,channel:d.channel||c.dm,reactions:[...p,n]}},await y(a.file,s)),a.file}),"reviewReact"),Kn=r(e=>h.join(A(e),"data","review-digests.json"),"digestFile");async function sr(e,t){A(e);const i=await xn.waiting("slack");if(i)return{waiting:i,dms:[],reactions:[],digest:null};const n=await M(e),a=await $(pn,{}),s=st(),o=`board.mjs --site ${h.basename(e)}`,c=r(l=>/^[\w.@-]+$/.test(l)?l:JSON.stringify(l),"arg"),d=[],f=[];for(const l of await q(e)){const w=await $(l.file,{}),m=Fe(w,n.reviewers),g=ee(e,w);for(const b of w.items||[]){const{prs:P,pr:D}=Hn(a,b,g);if(!D||["done","dropped"].includes(b.status))continue;const S=`${l.slug}/${b.id}`,_=at(w,b,S,s);if(b.status==="merged"||b.merged_at||kn(P.map(x=>x.gh)))for(const x of m){const O=J(_,x.name),R=J(b.review_link_ts,x.name);!O?.ts||Array.isArray(R?.reactions)&&R.reactions.includes("git_merged")||f.push({key:S,reviewer:x.name,channel:O.channel||x.dm,ts:O.ts,emoji:"git_merged",record:`${o} review react ${S} ${c(x.name)} git_merged`})}else if(D.gh?.state==="OPEN"&&!D.gh.isDraft){const x=P.filter(O=>O.gh?.state==="OPEN").map(O=>O.url).join(`
15
- `);for(const O of m.filter(R=>!J(_,R.name)))d.push({key:S,reviewer:O.name,slack_user:O.slack_user,channel:O.dm,text:x,record:`${o} review sent ${S} ${c(O.name)} --ts <ts> --channel ${O.dm}`})}}}const p=await ps(e,t);return{dms:d,reactions:f,digest:p.due?{...p,record:`${o} review digest sent <reviewer> --ts <ts> --channel <dm> --items <key,key>`}:null}}r(sr,"reviewDue");function fs(e,t){return new Promise((i,n)=>{Pt("gh",["pr","view",String(t),"--repo",e,"--json","latestReviews"],{encoding:"utf8",timeout:2e4},(a,s,o)=>{if(a)return n(new Q(`gh could not read ${e}#${t}'s reviews: ${String(o||a.message).trim().split(`
16
- `)[0]}`));i(Object.fromEntries((JSON.parse(s).latestReviews||[]).map(c=>[c.author?.login,c.state])))})})}r(fs,"ghLatestReviews");async function or(e,t,i,{fetch:n,reviews:a=fs}={}){A(e);const s=Na({fetch:n})||u(La),o=j(i,"what changed"),{wave:c,item:d}=await v(e,t),f=at(c,d,t,st()),p=Fe(c,(await M(e)).reviewers).filter(m=>f[m.name]?.ts);if(!p.length)return{lines:[`no reviewer waiting on a sent link for ${t}`]};const l=await Promise.all(Ne(d,ee(e,c)).map(m=>a(m.repo,m.number))),w=p.filter(m=>!l.length||l.some(g=>g[m.github]!=="APPROVED"));for(const m of w)try{await s.post({channel:f[m.name].channel||m.dm,thread_ts:f[m.name].ts,text:`\u{1F916} Updated: ${o}`})}catch(g){throw g instanceof Ea&&u(`Updated to ${m.name} on ${t} not sent: ${g.message}`),g}return{lines:[w.length?`\u{1F916} Updated sent to ${w.map(m=>m.name).join(", ")} on ${t}`:`every reviewer on ${t} has approved; nothing sent`]}}r(or,"reviewUpdated");function Hn(e,t,i){const n=Ne(t,i).map(a=>({...a,url:Ra(a),gh:J(e.prs,Aa(a))||null}));return{prs:n,pr:n[Da(n.map(a=>a.gh))]||null}}r(Hn,"cachedPRs");async function ps(e,{now:t=new Date}={}){A(e);const i=await M(e),n=await $(pn,{}),a=st(),s=new Map;for(const p of await q(e)){const l=await $(p.file,{}),w=Fe(l,i.reviewers);for(const g of w)s.has(g.name)||s.set(g.name,{name:g.name,slack_user:g.slack_user,dm:g.dm,items:[]});const m=ee(e,l);for(const g of l.items||[]){if(["merged","done","dropped"].includes(g.status))continue;const b=`${p.slug}/${g.id}`,{pr:P}=Hn(n,g,m),D=P?.gh;if(D?.state!=="OPEN"||D.isDraft||D.failing?.length||D.reviewDecision==="APPROVED")continue;const S=at(l,g,b,a);for(const _ of w){const x=J(S,_.name),O=x?.ts?(t.getTime()-Number(x.ts)*1e3)/864e5:null;s.get(_.name).items.push({key:b,title:g.title,pr:P.number,url:P.url,sent_ts:x?.ts||null,...O>2?{days_old:Math.floor(O)}:{}})}}}const{date:o,morning:c}=vn(t),d=await $(Kn(e),{}),f=await xn.waiting("slack");return{date:o,due:!f&&c&&d.last_date!==o,...f?{waiting:f}:{},reviewers:[...s.values()].filter(p=>p.items.length)}}r(ps,"reviewDigest");async function rr(e,t,{ts:i,channel:n,items:a},{now:s=new Date}={}){const o=Un(i,n),c=[...new Set(j(a,"--items").split(",").map(p=>p.trim()))],d=await M(e);for(const p of c){const{wave:l}=await v(e,p);Wt(l,d.reviewers,t)}const f=Kn(e);return nt(f,async()=>{const p=await $(f,{});return await y(f,{last_date:vn(s).date,digests:[...p.digests||[],{reviewer:t,dm:o.channel,ts:o.ts,items:c,reactions:[]}]}),f})}r(rr,"reviewDigestSent");function cr(e,t){const i=Ft(Array.isArray(t)?t:[t],"the reviewer file");return Mt(e,n=>[...n.map(a=>i.find(s=>We(s.name,a.name))||a),...i.filter(a=>!n.some(s=>We(a.name,s.name)))])}r(cr,"addReviewers");function dr(e,t){return Mt(e,i=>{const n=t.filter(a=>!i.some(s=>We(s.name,a)));return n.length&&u(`this board has no reviewer ${n.map(a=>`"${a}"`).join(", ")}`),i.filter(a=>!t.some(s=>We(a.name,s)))})}r(dr,"removeReviewers");const ws=r(()=>h.join(Ot.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),Ut=r(e=>{try{return e()}catch(t){throw t instanceof Ta&&u(t.message),t}},"ruleCall"),Kt=r(async e=>Sn(await $(h.join(e,"board.json"),{})),"readBoardRules");let Jn=Promise.resolve();function pt(e,t){const i=Jn.then(async()=>{const n=h.join(A(e),"board.json"),a=await $(n,{}),{set:s,...o}=t(a);return await y(n,{...a,...s}),{file:n,...o}});return Jn=i.catch(()=>{}),i}r(pt,"writeBoardJSON");const zn=r((e,t)=>pt(e,i=>{const n=Ut(()=>t(Sn(i)));return{set:{rules:n.rules},...n}}),"writeBoardRules");function lr(e,t){const i=Object.keys(t||{}),[n]=i,a=typeof t?.[n]=="string"?t[n].trim():null;return(i.length!==1||!["add","remove"].includes(n)||!Ha(a))&&u("Send add or remove with one login, like name@github."),pt(e,s=>{const o=Ka(s),c=n==="remove"?o.filter(d=>d!==a):o.includes(a)?o:[...o,a];return{set:{share:c},share:c}}).then(({share:s})=>({share:s}))}r(lr,"saveBoardShare");const Gn=r(e=>typeof e=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e),"validGithubLogin"),Qn=r(e=>{try{return e?Et(e):null}catch{return null}},"storedTickets"),Ht=r(e=>Number.isInteger(e)&&e>=1&&e<=99,"validCap"),Jt=["max_implementers","max_planners"],ge=r(e=>{const t=Qn(e?.tickets);return{auto_merge:e?.auto_merge===!0,auto_build:e?.auto_build===!0,auto_pull:e?.auto_pull===!0,max_implementers:Ht(e?.max_implementers)?e.max_implementers:Gi,max_planners:Ht(e?.max_planners)?e.max_planners:Qi,auto_triage:e?.auto_triage===!0,auto_review:e?.auto_review===!0,idle_notification:e?.idle_notification===!0,skip_draft:e?.skip_draft===!0,prs:e?.prs!==!1,...t?{tickets:t}:{},...Gn(e?.github_login)?{github_login:e.github_login}:{}}},"settingsOf");async function ms(e){return ge(await $(h.join(A(e),"board.json"),{}))}r(ms,"readBoardSettings");const ur=r((e,t)=>!!e?.github&&!e.local&&t.prs,"opensPRs");async function Ue(e,t){Ee(t,["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","tickets","github_login"],"the settings"),Object.keys(t).length||u("the settings name nothing to change"),t={...t};for(const[i,n]of Object.entries(t))if(i==="tickets"){try{t.tickets=Et(n)}catch(s){u(s.message)}t.tickets.source==="linear"&&!ka()&&u(ga);const a=t.tickets.source==="github"&&e?we().find(s=>ve(s.dir,e)):null;a&&!a.github&&u("GitHub issues need the board\u2019s repo on GitHub, and this board\u2019s repo isn\u2019t; pick internal or linear")}else i==="github_login"?n===""||n===null?t.github_login=void 0:Gn(n)||u("github_login must be a GitHub login"):Jt.includes(i)?Ht(n)||u(`${i} must be a whole number from 1 to 99`):dt(n,i);if(t.prs===!1){const i=e?we().find(n=>ve(n.dir,e)):null;i?.github&&!i.local&&!(await Ca(i.owner?.repo)).pushMain&&u("Merge lands each card on main and pushes it, so turn on Push main after a land first")}return pt(e,i=>{const n=ge(i),a=ge({...i,...t});return{set:t,settings:a,changed:Object.keys(t).filter(s=>JSON.stringify(n[s])!==JSON.stringify(a[s]))}})}r(Ue,"saveBoardSettings");async function fr(){if($a().set)return[];const e=[];for(const t of we()){if((await ms(t.dir)).tickets?.source!=="linear")continue;const{settings:i}=await Ue(t.dir,{tickets:{source:"internal"}});await pe(t,"settings",{tickets:i.tickets}),e.push(`/${t.route} fell back to internal tickets: Hub has no Linear key`)}return e}r(fr,"fallBackFromLinear");async function pr(e,t,i){const n=String(t).replace(/-/g,"_"),a=["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","github_login","tickets"];if(a.includes(n)||u(`no board setting "${t}"; settings are ${a.map(c=>c.replace(/_/g,"-")).join(", ")}`),n==="tickets"){let c;try{c=JSON.parse(i)}catch{u('settings set tickets takes the source as JSON, such as {"source":"github"}')}const{file:d,settings:f}=await Ue(e,{tickets:c});return{file:d,lines:hs({tickets:f.tickets})}}if(n==="github_login"){const{file:c,settings:d}=await Ue(e,{github_login:i});return{file:c,lines:[`github-login ${d.github_login||"unset"}`]}}if(Jt.includes(n)){const{file:c,settings:d}=await Ue(e,{[n]:/^\d+$/.test(i)?Number(i):i});return{file:c,lines:[`${t} ${d[n]}`]}}["on","off"].includes(i)||u(`settings set ${t} takes on or off, not "${i}"`);const{file:s,settings:o}=await Ue(e,{[n]:i==="on"});return{file:s,lines:[`${t} ${o[n]?"on":"off"}`]}}r(pr,"setBoardSetting");const hs=r(e=>Object.entries(e).map(([t,i])=>`setting ${t.replace(/_/g,"-")} ${t==="tickets"?JSON.stringify(i):t==="github_login"||Jt.includes(t)?i:i?"on":"off"}`),"settingsLines");async function zt(e,t,i){const n=t?Ut(()=>i(ot())):await zn(e,i);return t&&Ut(()=>Ua(n.rules)),{file:t?ws():n.file,lines:[`id ${n.rule.id}`]}}r(zt,"changeRules");async function Zn(e,t,i){const n=r(o=>o.map(c=>c.id),"ids");i||A(e);const[a,s]=i?[n(ot()),e?n(await Kt(e)):[]]:[n(await Kt(e)),n(ot())];a.includes(t)||u(`no ${i?"hub":"board"} rule "${t}"${s.includes(t)?`; it is a ${i?"board":"hub"} rule, so ${i?"drop":"pass"} --hub`:""}`)}r(Zn,"checkRuleId");async function wr(e){const t=r(i=>n=>`${i} ${n.id} ${n.by==="claude"?"claude":"owner"} ${n.text}`,"line");return{lines:[...ot().map(t("hub")),...e?(await Kt(e)).map(t("board")):[]]}}r(wr,"listRules");function mr(e,{text:t,hub:i=!1,by:n}){return i||A(e),zt(e,i,a=>qa(a,{text:t,by:n}))}r(mr,"addRuleTo");async function hr(e,t,{text:i,hub:n=!1}){return await Zn(e,t,n),zt(e,n,a=>Fa(a,t,i))}r(hr,"editRuleIn");async function gr(e,t,{hub:i=!1}){return await Zn(e,t,i),zt(e,i,n=>Ma(n,t))}r(gr,"removeRuleFrom");const $r=r((e,t,i)=>zn(e,n=>Wa(n,t,i)),"saveBoardRules");async function kr(e,{id:t,title:i,summary:n}){A(e);const a=await $(L(e),{topics:[],items:[]}),s=a.topics||[];return s.some(o=>o.id===j(t,"the topic id"))&&u(`topic "${t}" already exists`),s.push({id:t.trim(),title:j(i,"--title"),...n===void 0?{}:{summary:j(n,"--summary")}}),await y(L(e),{...a,topics:s,items:a.items||[]}),L(e)}r(kr,"addTopic");const Vn=["quiet","info","notice","danger","success"];function gs(e,t){if(e.diagram===void 0!=(e.practice===void 0)&&u(`${t} needs diagram and practice together`),e.diagram===void 0)return;const i=e.diagram;(!Array.isArray(i)||!i.length)&&u(`${t}.diagram must be a non-empty array of rows`),i.forEach((n,a)=>{(!Array.isArray(n)||!n.length||n.length>4)&&u(`${t}.diagram[${a}] must be a row of 1 to 4 boxes`),n.forEach((s,o)=>{const c=`${t}.diagram[${a}][${o}]`;Ee(s,["text","tone","to"],c),j(s.text,`${c}.text`),z(s.tone,d=>Vn.includes(d)?d:u(`${c}.tone takes ${Vn.join(", ")}`)),z(s.to,d=>Array.isArray(d)&&a+1<i.length&&d.every(f=>Number.isInteger(f)&&f>=0&&f<i[a+1].length)?d:u(`${c}.to must list boxes of the next row by index`))})}),(!Array.isArray(e.practice)||!e.practice.length)&&u(`${t}.practice must be a non-empty array of lines`),e.practice.forEach((n,a)=>j(n,`${t}.practice[${a}]`))}r(gs,"checkDiagram");function $s(e,t){(!Array.isArray(e)||!e.length)&&u(`${t}.options must be a non-empty array`);const i=new Set;e.forEach((n,a)=>{const s=`${t}.options[${a}]`;Ee(n,["id","label","recommended","detail","diagram","practice"],s),gs(n,s),i.has(j(n.id,`${s}.id`))&&u(`${t} has two options with id "${n.id}"`),i.add(n.id),j(n.label,`${s}.label`).startsWith(`${n.id}. `)&&u(`${s}.label starts with "${n.id}. ", but the page adds the option letter itself`),z(n.recommended,o=>dt(o,`${s}.recommended`)),z(n.detail,o=>typeof o=="string"?o:u(`${s}.detail must be text`))}),e.filter(n=>n.recommended).length>1&&u(`${t} recommends more than one option`)}r($s,"checkOptions");function ks(e,t,i){Ee(e,["option","card","answer"],i),t.some(n=>n.id===j(e.option,`${i}.option`))||u(`${i}.option names no option "${e.option}"`),j(e.card,`${i}.card`),j(e.answer,`${i}.answer`)}r(ks,"checkSuggested");const Ke=r(e=>h.join(e,"data","prototypes.json"),"prototypesFile"),Gt=r((e,t,i)=>h.join(e,"public","prototypes",t,`${i}.js`),"takeFile");async function wt(e){const t=e?(await $(Ke(e),{})).prototypes:null;return Array.isArray(t)?t:[]}r(wt,"readPrototypes");const ys=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:u(`a prototype topic uses only a-z, 0-9 and "-", not "${e}"`),"prototypeTopic");function bs(e){const t=/^([^:]*):(.*)$/s.exec(e);(!t||!/^[A-Z]$/.test(t[1]))&&u(`--take takes KEY:"Label" with KEY one capital letter, not "${e}"`);const i=j(t[2],`--take ${t[1]}'s label`);return/^[A-Z]\.\s/.test(i)&&u(`--take ${t[1]}'s label starts with "${i.slice(0,3)}"; the take pill shows the label alone, so drop the letter`),{key:t[1],label:i}}r(bs,"prototypeTake");const Ie=r(e=>/^prototype-(.+)\.html$/.exec(h.posix.basename(String(e||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function yr(e,{topic:t,title:i,card:n,takes:a=[]}){A(e),ys(t),j(i,"--title"),a.length||u('prototype add needs a --take KEY:"Label" for each take');const s=a.map(bs),o=s.find((f,p)=>s.findIndex(l=>l.key===f.key)!==p);o&&u(`--take ${o.key} is given twice`),n!==void 0&&await v(e,n);const c=await wt(e);c.some(f=>f.topic===t)&&u(`prototype ${t} is already recorded; run prototype rm ${t} first, or pick another topic`),await N.stat(h.join(e,"public",`prototype-${t}.html`)).then(()=>!0,()=>!1)&&u(`public/prototype-${t}.html is a hand-written page that questions may link; pick another topic`);const d={topic:t,title:i.trim(),...n===void 0?{}:{card:n},takes:s,at:new Date().toISOString()};return await y(Ke(e),{prototypes:[...c,d]}),{file:Ke(e),lines:[`page prototype-${t}.html`,...s.map(f=>`take ${f.key} ${Gt(e,t,f.key)}`)]}}r(yr,"addPrototype");async function br(e){A(e);const t=await wt(e);return{lines:t.length?t.map(i=>`${i.topic} \xB7 ${i.title} \xB7 ${i.takes.map(n=>`${n.key}: ${n.label}`).join(", ")}${i.card?` \xB7 ${i.card}`:""}`):["no prototypes recorded"]}}r(br,"listPrototypes");async function xr(e,t){A(e);const i=await wt(e);i.some(s=>s.topic===t)||u(`no prototype "${t}" is recorded`);const a=((await $(L(e),{items:[]})).items||[]).filter(s=>!s.resolved&&Ie(s.url)===t).map(s=>s.id);return a.length&&u(`prototype ${t} backs open question ${a.join(", ")}; resolve ${a.length===1?"it":"them"} first`),await y(Ke(e),{prototypes:i.filter(s=>s.topic!==t)}),Ke(e)}r(xr,"removePrototype");async function xs(e,t,i){const n=t.url?.split(/[?#]/)[0];if(!n||!h.posix.basename(n).startsWith("prototype-"))return;const a=h.posix.basename(n),s=t.url.split("#")[0].split("?").slice(1).join("?"),o=`${i}.url`;new URLSearchParams(s).get("q")!==t.id&&u(`${o} must be ${a}?q=${t.id} so the pill can pick`);const c=Ie(n),d=(await wt(e)).find(w=>w.topic===c);d||u(`${o} names ${a}, which is not a recorded prototype; record it with board.mjs prototype add ${c} --title "\u2026" --take A:"\u2026", then write its take files`);const f=d.takes.map(w=>w.key),p=t.options.map(w=>w.id),l=p.map(w=>f.indexOf(w));l.some((w,m)=>w<0||m>0&&w<l[m-1])&&u(`${i}.options ids ${p.join(", ")} must be a non-empty subset of ${a}'s takes, in take order: ${f.join(", ")}`);for(const w of f)await N.stat(Gt(e,c,w)).then(m=>m.isFile(),()=>!1)||u(`${o} take ${w} has no file yet; write ${Gt(e,c,w)}`)}r(xs,"checkPrototype");async function vr(e,t){A(e);const i=Array.isArray(t)?t:[t];i.length||u("the question file is empty");const n=await $(L(e),{topics:[],items:[]}),a=n.items||[],s=await $(Me(e),{}),o=new Set([...a,...Object.values(s).flat()].map(p=>p.id)),c=new Set((n.topics||[]).map(p=>p.id));for(const[p,l]of i.entries()){const w=`question ${l?.id??p+1}`;Ee(l,["id","topic","source","question","context","url","urgent","options","round_of","reask_of","ask","suggested","prototype_gate"],w),o.has(me(l.id,`${w}.id`))&&u(`question id "${l.id}" is already used`),o.add(l.id),j(l.topic,`${w}.topic`),l.topic.startsWith("item:ask:")?await Je(e,l.topic.slice(9),`${w}.topic`):l.topic.startsWith("item:")?await v(e,l.topic.slice(5)):l.topic.startsWith("lineup:")?await xt(e,l.topic.slice(7)):l.topic.startsWith("wave:")?await Us(e,l.topic.slice(5)):c.has(l.topic)||u(`topic "${l.topic}" is not declared; add it with: topic add ${l.topic} --title <t>`),j(l.question,`${w}.question`);for(const m of["source","context"])z(l[m],g=>j(g,`${w}.${m}`));z(l.url,m=>jn(m,`${w}.url`)),z(l.urgent,m=>dt(m,`${w}.urgent`)),$s(l.options,w),z(l.prototype_gate,m=>vs(m,l,w)),z(l.suggested,m=>ks(m,l.options,`${w}.suggested`)),await xs(e,l,w),l.ask!==void 0&&await eo(e,l,w),z(l.round_of,m=>{const g=a.find(b=>b.id===m);g||u(`${w}.round_of names no question "${m}" in owner.json`),g.topic!==l.topic&&u(`${w}.round_of names ${m} on topic ${g.topic}; a round stays on its question's topic, ${l.topic}`),Ie(l.url)||u(`${w}.round_of needs a url on a recorded prototype, prototype-<topic>.html?q=${l.id}`)}),z(l.reask_of,m=>{const g=a.find(b=>b.id===m);g||u(`${w}.reask_of names no question "${m}" in owner.json`),g.topic!==l.topic&&u(`${w}.reask_of names ${m} on topic ${g.topic}; a re-ask stays on its question's topic, ${l.topic}`),g.resolved||u(`${w}.reask_of names ${m}, which is not resolved; only a resolved question is asked again`)})}const d=[],f=[];for(const p of i){const l=p.topic.startsWith("item:")&&!p.topic.startsWith("item:ask:")?p.topic.slice(5):null,w=l&&await Yn(e,l);w&&((s[l]||=[]).push(p),f.push(p),d.push(`staged: ${p.id} waits for ${l}'s ${w} to finish; the card's staged questions then publish together`))}return f.length&&await y(Me(e),s),f.length===i.length?{file:Me(e),lines:d,staged:f.length}:Xn(e,n,a,i.filter(p=>!f.includes(p)),d)}r(vr,"addQuestions");async function Yn(e,t){const{source:i}=await v(e,t);for(const n of await te(i.dir,C(t).id)){const a=await $(n,null);if(a?.started&&!a.done&&Re(a))return a.kind}return null}r(Yn,"runningPlanner");async function mt(e,t){if(!e||await Yn(e,t))return;const i=await $(Me(e),{});if(!i[t])return;const{[t]:n,...a}=i,s=await $(L(e),{topics:[],items:[]});await Xn(e,s,s.items||[],n,[]),await y(Me(e),a)}r(mt,"publishStaged");async function Xn(e,t,i,n,a){const{answers:s,requests:o}=await He(e),c=new Date().toISOString(),d=[],f=[];for(const l of n.filter(w=>w.round_of))l.round=Nt(i.find(w=>w.id===l.round_of))+1,$e(t,[l.round_of],`Followed by prototype round ${l.round}`,c),f.push(l.round_of);for(const l of n.filter(w=>w.reask_of))$e(t,[l.reask_of],`Asked again as ${l.id}`,c),f.push(l.reask_of);for(const l of n){const w=Ie(l.url),m=i.filter(b=>b.topic===l.topic).map(b=>Ae(b,s,o)),g=l.topic.startsWith("item:")?ei(m):[];for(const b of m)g.includes(b.id)?($e(t,[b.id],`Followed by ${l.id}`,c),f.push(b.id)):!b.resolved&&w&&Ie(b.url)===w&&($e(t,[b.id],`Replaced by ${l.id}`,c),d.push(b.id))}const p={...t,topics:t.topics||[],items:[...i,...n.map(l=>({...l,asked_at:c}))]};return await y(L(e),p),{file:L(e),lines:[...a,...d.map(l=>`replaced: ${l}`),...f.map(l=>`resolved: ${l}`)]}}r(Xn,"publishQuestions");function vs(e,t,i){dt(e,`${i}.prototype_gate`),e&&((!t.topic.startsWith("item:")||t.topic.startsWith("item:ask:"))&&u(`${i}.prototype_gate goes on a card's topic, item:<wave>/<id>`),t.options?.map(n=>n.id).join()!=="yes,no"&&u(`${i}.prototype_gate takes exactly the options yes and no, in that order`))}r(vs,"checkGate");async function Sr(e,t){const i=await $(L(e),{topics:[],items:[]}),n=new Map((i.items||[]).map(o=>[o.id,o])),a=new Set(t.map(o=>n.get(String(o.id))?.topic).filter(o=>o?.startsWith("item:"))),s=[];for(const o of t){const c=n.get(String(o.id));if(!c?.prototype_gate||!["yes","no"].includes(o.choice)||($e(i,[c.id],`Prototype round: ${o.choice}. The board moved the card on.`),await y(L(e),i),o.choice==="no"))continue;a.delete(c.topic);const d=c.topic.slice(5);(await v(e,d)).item.state==="grilling"&&(await se(e,d,{state:"prototyping"}),await re(e,d,{text:"You asked for a prototype round, so the board moved the card to prototyping."}),s.push(d))}for(const o of a){const{asked:c}=await Zt(e,o),d=c.find(p=>p.prototype_gate&&p.answers.at(-1)?.choice==="no"),f=o.slice(5);if(!(!d||c.some(p=>p.state==="open")||(await v(e,f)).item.state!=="grilling"))try{await se(e,f,{state:"ready"}),await re(e,f,{text:"No prototype round, and nothing left to answer, so the board moved the card to ready."})}catch(p){if(!(p instanceof Q))throw p;await re(e,f,{text:`No prototype round, but the card isn't ready yet: ${p.message}`})}}return{prototype:s}}r(Sr,"settleGates");function ei(e){return e.filter(t=>t.state==="answered").map(t=>t.id)}r(ei,"answeredOnClaude");function $e(e,t,i,n=new Date().toISOString()){for(const a of e.items||[])t.includes(a.id)&&Object.assign(a,{resolved:i,resolved_at:n})}r($e,"setResolved");async function Qt(e,t,i,n){return $e(t,i,n),await y(L(e),t),L(e)}r(Qt,"markResolved");async function _r(e,t,i){A(e);const n=await $(L(e),{topics:[],items:[]}),a=(n.items||[]).find(p=>p.id===t);a||u(`no question "${t}" in owner.json`);const{answers:s,requests:o}=await He(e),c=Ae(a,s,o);if(c.state==="resolved")return L(e);c.state==="requested"&&u(`${t} waits on the prototype the owner asked for; question add with round_of ${t} resolves it`);const d=j(i,"--note");Ie(a.url)&&Ss(n,a,s);const f=await Qt(e,n,[t],d);return await ai(e,a.topic),f}r(_r,"resolveQuestion");async function jr(e,t,i,n=[]){A(e);const[a,...s]=[...new Set(t)];a||u("question move takes the ids of the questions to merge"),await v(e,i);const o=[i,...n];new Set(o).size!==o.length&&u("question move names a card twice");for(const l of n)await v(e,l);const c=await $(L(e),{topics:[],items:[]}),{answers:d,requests:f}=await He(e);for(const l of[a,...s]){const w=(c.items||[]).find(g=>g.id===l)||u(`no question "${l}" in owner.json`),{state:m}=Ae(w,d,f);m!=="open"&&u(`${l} is ${m}; question move takes open questions`)}for(const[l,w]of n.entries()){const{item:m}=await v(e,w),g=[...m.deps||[],o[l]].join(",");await se(e,w,{deps:g,relate:i})}const p=await X(e,i);return Object.assign(c.items.find(l=>l.id===a),{topic:`item:${i}`}),$e(c,s,`Duplicate of ${p}'s question ${a}`),await y(L(e),c),L(e)}r(jr,"moveQuestions");async function Or(e,t){const i=t.filter(({choice:a,note:s})=>!a&&String(s||"").trim()).map(a=>a.id),n=i.length?await $(L(e),{topics:[],items:[]}):null;n?.items?.some(a=>i.includes(a.id))&&($e(n,i,"Answered with a note"),await y(L(e),n))}r(Or,"resolveNoteAnswers");function Ss(e,t,i){const n=Nt(t),a=e.items.filter(s=>s.topic===t.topic&&Nt(s)===n).map(s=>Ae(s,i));ea(a,n)||u(`${t.id} is in prototype round ${n}, which no pick has settled yet; a pick settles it`)}r(Ss,"roundSettled");async function He(e){const t=await G(h.join(e,"data","events.jsonl"));return{answers:await Promise.all(t.filter(n=>n.kind==="owner-answer").map(async n=>{const a=await Promise.all(["html","json"].map(s=>N.stat(h.join(e,"data","replies",`${n.id}.${s}`)).then(o=>o.isFile(),()=>!1)));return{...n,reply:n.reply||a.some(Boolean)}})),requests:t.filter(n=>n.kind==="prototype-request")}}r(He,"ownerEvents");async function Zt(e,t){const i=await $(L(e),{topics:[],items:[]}),{answers:n,requests:a}=await He(e),s=(i.items||[]).filter(o=>o.topic===t).map(o=>Ae(o,n,a));return{owner:i,asked:s}}r(Zt,"topicQuestions");const ke=r((e,t)=>Zt(e,`item:${t}`),"cardQuestions"),Vt=r((e,t,i)=>ti(e,`item:${t}`,i),"resolveAnswered");async function ti(e,t,i){if(!e)return[];const{owner:n,asked:a}=await Zt(e,t),s=ei(a);return s.length&&await Qt(e,n,s,i),await ai(e,t),s.map(o=>`resolved: ${o}`)}r(ti,"resolveAnsweredOn");const ni=r(async(e,t)=>!!e&&(await ke(e,t)).asked.some(i=>i.state==="open"),"hasOpenQuestion");function ii(e,t,i){e.next=j(t,"--next"),i?e.next_on_questions=!0:delete e.next_on_questions}r(ii,"setNext");async function ai(e,t){const i=t.startsWith("item:")&&!t.startsWith("item:ask:")?t.slice(5):null;if(!i)return;let n;try{n=await v(e,i)}catch(c){if(c instanceof Q)return;throw c}const{source:a,wave:s,item:o}=n;o.next_on_questions&&((await ke(e,i)).asked.some(c=>c.state!=="resolved")||(delete o.next,delete o.next_on_questions,await y(a.file,s)))}r(ai,"clearWaitedNext");async function _s(e,t){const{source:i,wave:n,item:a}=await v(e,t);a.next===void 0&&a.next_on_questions===void 0||(delete a.next,delete a.next_on_questions,await y(i.file,n))}r(_s,"clearNext");async function si(e,t,i,n){if(!e)return async()=>[];const{owner:a,asked:s}=await ke(e,t),o=s.filter(d=>d.state==="open").map(d=>d.id);o.length&&!n&&u(`${i}: ${t} has ${o.length===1?"an open question":"open questions"} ${o.join(", ")} with no answer yet; pass --force to close the card and resolve ${o.length===1?"it":"them"} too`);const c=s.filter(d=>d.state!=="resolved").map(d=>d.id);return async()=>(c.length&&await Qt(e,a,c,"Closed with the card"),c.map(d=>`resolved: ${d}`))}r(si,"closeQuestions");async function Pr(e,t,i,n,a){await v(e,t),j(i,"--text"),z(n,s=>j(s,"--owner-step")),z(a,s=>gi(s)),await si(A(e),t,"land",!1)}r(Pr,"checkClose");async function oi(e,t){const i=await $(L(e),{items:[]}),{answers:n,requests:a}=await He(e);return Vi((i.items||[]).map(s=>Ae(s,n,a)),t)}r(oi,"unpickedPrototype");const js=r(e=>e.length>120?`${e.slice(0,119)}\u2026`:e,"clip"),Os={ask:"text","ask-edited":"text","item-message":"text","owner-answer":"note"};async function ri(e,t,i){const n=j(t,i).replace(/\s+/g," ");return(await G(h.join(e,"data","events.jsonl"))).map(s=>String(s.data?.[Os[s.kind]]??"").replace(/\s+/g," ").toLowerCase()).some(s=>s.includes(n.toLowerCase()))||u(`${i} must quote the owner's words in an ask, card message or answer note on this board`),n}r(ri,"ownerQuote");async function Dr(e,t,i){return A(e),i===void 0&&u(`only the owner's Serve / Build and the hub's Auto-build start a card run; pass --asked "<the owner's words>" quoting where the owner asked for ${t}'s run`),`Running at the owner's ask: "${await ri(e,i,"--asked")}"`}r(Dr,"askedGate");async function Je(e,t,i="--from-ask"){return me(t,i),(await G(h.join(A(e),"data","events.jsonl"))).some(a=>a.id===t&&a.kind==="ask")?t:u(`no ask "${t}" in this site's data/events.jsonl`)}r(Je,"checkAsk");const I=["done","merged","dropped"],Ar=r(async(e,t)=>I.includes((await v(e,t)).item.status),"isFinished"),ci=["ready","grilling","prototyping"],Ps=/^mark (?:it |this |the card )?ready\b/i;function ht(e){return["building","fixing"].includes(e?.status)?(delete e.status,e.status_at=new Date().toISOString(),!0):!1}r(ht,"clearBuildingStatus");function Ds(e,t,i){const n=[].concat(t).map(o=>yn(o)||u(`--pr takes a PR number or owner/repo#<n>, not "${o}"`)),a=r((o,c)=>o.number===c.number&&(o.repo||i||null)===(c.repo||i||null),"same"),s=bn(e);for(const o of n)s.some(c=>a(c,o))||s.push(o);delete e.pr,e.prs=s}r(Ds,"addPRs");const W=r((e,t)=>String(t).includes("/")?String(t):`${e}/${t}`,"depKey");async function ze(e,t,i){const n=(i.deps||[]).map(s=>W(t,s)),a=await Promise.all(n.map(s=>v(e,s).then(({item:o})=>o,()=>null)));return n.filter((s,o)=>a[o]&&!I.includes(a[o].status))}r(ze,"unfinishedDeps");async function As(e,t){const i=[];for(const n of await q(e)){const a=await $(n.file,null);for(const s of Array.isArray(a?.items)?a.items:[])![void 0,"waiting","held"].includes(s.status)||!(s.deps||[]).some(o=>W(n.slug,o)===t)||(await ze(e,n.slug,s)).length||i.push(`${n.slug}/${s.id}`)}return i}r(As,"unblockedBy");async function di(e,t,i,n){const a=new Set(n.match(/[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),s=new Set((i.deps||[]).map(o=>W(C(t).wave,o)));for(const o of await q(e)){const c=await $(o.file,null);for(const d of c?.items||[]){const f=`${o.slug}/${d.id}`;if(f===t||s.has(f)||I.includes(d.status))continue;[d.id,tt(c,d)].flatMap(l=>[l,`${o.slug}/${l}`]).some(l=>a.has(l))&&u(`--next names unfinished card ${f}; add it with --deps`)}}}r(di,"checkNextDeps");function li(e,t){const i="a builder starts from it, so pass";t.size||u(`${e} has no size; ${i} --size small or large with --state ready`),t.plan||u(`${e} has no notes file; ${i} --notes-file <file.md> with --state ready`)}r(li,"checkSizeAndNotes");async function Rs(e,t,i,n){li(t,n);const a=await N.readFile(await Ge(i,n.plan),"utf8"),s=(await ke(e,t)).asked.filter(o=>o.answers.length&&!a.includes(o.id)&&!(o.prototype_gate&&!o.answers.at(-1).note)).map(o=>o.id);s.length&&u(`${t}'s notes leave out the answered question${s.length===1?"":"s"} ${s.join(", ")}; record each answer in ${n.plan}, naming its id, before it is ready`)}r(Rs,"checkNotesComplete");async function Ge(e,t){const i=r(a=>u(`a notes file is a .md file inside ${e}, named relative to it: "${t}" ${a}`),"bad");(typeof t!="string"||!t.trim())&&i("is empty"),(h.isAbsolute(t)||t.split(/[\\/]/).includes(".."))&&i("leaves the folder"),h.extname(t).toLowerCase()!==".md"&&i("is not a .md file");const n=await N.realpath(h.resolve(e,t)).catch(()=>i("does not exist"));return n.startsWith(await N.realpath(e)+h.sep)||i("leaves the folder"),n}r(Ge,"planPath");async function Rr(e,t){const{source:i,wave:n,item:a}=await v(e,t);if(a.plan)return{wave:n,item:a,markdown:await N.readFile(await Ge(i.dir,a.plan),"utf8")};if(typeof a.notes=="string"&&a.notes.trim())return{wave:n,item:a,markdown:a.notes};u(`${t} has no notes`)}r(Rr,"readNotes");function ui({install:e,installSha:t,installLabel:i,clearInstall:n}){if(e===void 0)return(t!==void 0||i!==void 0)&&u("--install-sha and --install-label go with --install <url>"),n?null:void 0;n&&u("pass --install or --clear-install, not both");let a;try{a=new URL(String(e).trim())}catch{a=null}(!a||!["https:","itms-services:"].includes(a.protocol))&&u(`--install takes an https:// or itms-services:// URL, not "${e}"`),a.protocol==="itms-services:"&&!(a.searchParams.get("action")==="download-manifest"&&/^https:\/\//.test(a.searchParams.get("url")||""))&&u("--install: an itms-services:// URL needs ?action=download-manifest&url=https://\u2026/manifest.plist");const s=t===void 0?void 0:/^[0-9a-f]{7,40}$/i.test(t)?t.toLowerCase():u(`--install-sha takes a commit sha of 7 to 40 hex digits, not "${t}"`);return{url:String(e).trim(),...i===void 0?{}:{label:j(i,"--install-label")},built_at:new Date().toISOString(),...s?{sha:s}:{}}}r(ui,"installOf");async function Nr(e,{install:t,installSha:i}){const n=ui({install:t,installSha:i});n||u("main set needs --install <url>");const a=h.join(A(e),"data","main.json");return await y(a,{install:n}),{file:a,lines:["main install set"]}}r(Nr,"setMainInstall");const fi=r((e,t)=>e.length?`Merged PR${e.length>1?"s":""} ${qe(e,t)}.`:"Merged.","mergedLine");function Ns(e,t,i,n,a,s){return n==="merged"?a??(i.status==="merged"?null:fi(bn(i),s)):(a===void 0&&e&&u(`--status ${n} needs --text "<what was done or left>", which becomes ${t}'s last thread line`),a??null)}r(Ns,"closingLine");async function pi(e,t,{ticket:i,status:n,clearStatus:a,next:s,clearNext:o,pr:c,pushed:d,mergeReady:f,ghSince:p,fromAsk:l,state:w,held:m,unheld:g,pickUpAt:b,notesFile:P,deps:D,parent:S,related:_,relate:x,unrelate:O,install:R,installSha:Z,installLabel:ne,clearInstall:F,title:B,size:oe,model:ie,force:ce,text:ae,who:le,ownerStep:de,ownerDone:vt,check:V},ue){n!==void 0&&a&&u("pass --status or --clear-status, not both"),_!==void 0&&(x!==void 0||O!==void 0)&&u("pass --related, or --relate and --unrelate, not both"),w!==void 0&&!ci.includes(w)&&u("--state takes ready, grilling or prototyping"),ce&&!I.includes(n)&&u(`--force goes with --status ${I.join(", ")}`),l!==void 0&&I.includes(n)&&u("--from-ask opens a card; close it in another call"),ae!==void 0&&j(ae,"--text"),de!==void 0&&j(de,"--owner-step");const ye=V===void 0||V===""?V:gi(V);ae!==void 0&&l===void 0&&!I.includes(n)&&u(`--text goes with --from-ask or --status ${I.join(", ")}`),l!==void 0&&ae===void 0&&u(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),s!==void 0&&o&&u("pass --next or --clear-next, not both"),typeof s=="string"&&!s.trim()&&([s,o]=[void 0,!0]);const Pe=Es({held:m,unheld:g,pickUpAt:b}),be=ui({install:R,installSha:Z,installLabel:ne,clearInstall:F});[n,a,s,o,c,d,f,p,l,w,Pe,P,D,S,_,x,O,be,B,oe,ie,i,de,V].every(E=>E===void 0||E===!1)&&u("nothing to set: pass --status, --clear-status, --next, --clear-next, --pr, --pushed, --gh-since, --from-ask, --state, --held, --unheld, --pick-up-at, --notes-file, --deps, --parent, --related, --relate, --unrelate, --install, --clear-install, --title, --size, --model, --ticket or --check");const H=l===void 0?void 0:await Je(e,l);H&&await Tt(e,H);const{source:T,wave:xe,item:k}=await v(e,t);H&&k.from_ask&&k.from_ask!==H&&u(`${t} came from ask "${k.from_ask}", so ask "${H}" can't be its from-ask; to fold it in: item relate-ask ${H} ${t} --summary "<line>"`),H&&w===void 0&&!k.state&&u("--from-ask files a card, which reads New until it has a state: pass --state ready, grilling or prototyping");const Mi=k.status,on=I.includes(k.status);de!==void 0&&on&&u(`${t} is already ${k.status}, so its ticket closed; an owner step goes on the command that finishes the card: land --owner-step, or builder done before the merge`),de!==void 0&&(k.owner_step=j(de,"--owner-step")),(vt||n==="dropped")&&delete k.owner_step,ye!==void 0&&!k.owner_step&&u(`--check goes with an owner step, which ${t} doesn't have: pass --owner-step "<the step>" with it on builder done or land`),ye&&(k.owner_check={check:ye,since:k.owner_check?.check===ye?k.owner_check.since:new Date().toISOString()}),(ye===""||!k.owner_step)&&delete k.owner_check;const rn=await Promise.all((await te(T.dir,k.id)).map(E=>$(E,null))),Wi=Rt(k,rn);if(P!==void 0&&(await Ge(T.dir,P),k.plan=h.normalize(P)),B!==void 0&&(k.title=j(B,"--title")),wi(k,{size:oe,model:ie}),D!==void 0){const E=await $i(e,C(t).wave,D);E.length?k.deps=E:delete k.deps}if(S!==void 0){const E=await ki(e,C(t).wave,t,S);E?k.parent=E:delete k.parent}let St=_===void 0?null:await Is(e,t,k,_);if(x!==void 0&&(k.related=[...new Set([...(k.related||[]).map(E=>W(C(t).wave,E)),...await $t(e,t,x,"--relate")])]),O!==void 0&&(St=await Bs(e,t,k,await $t(e,t,O,"--unrelate"))),Pe!==void 0&&gt(k,Pe),n==="building"&&(k.held||k.status==="held")&&u(k.pick_up_at?`${t} is held until ${k.pick_up_at}, when the board resumes it; start it early only with item set ${t} --unheld`:`${t} is held; start it only once the owner resumes it, with item set ${t} --unheld`),n==="planned"&&u("--status planned is gone: an unstarted card has no status. Clear it with --clear-status"),n!==void 0&&(k.status=fn.includes(n)?n:u(`--status takes ${fn.join(", ")}`)),(Ls.includes(n)||c!==void 0||d!==void 0||f!==void 0)&&Cs(k),n==="building"){const E=await ze(e,C(t).wave,k);E.length&&u(`${t} is blocked by ${E.join(", ")}; start it once ${E.length===1?"that is":"those are"} done or merged`)}(n==="building"||I.includes(n))&&delete k.implement_at;const Ve=n==="building"&&e?await oi(e,t):null;Ve&&u(`${t} has no take picked: the owner's answer to ${Ve.id} picks none${Ve.note?` ("${js(Ve.note)}")`:""}, so it prototypes again: draw the next round from that note, or grill when the note asks a question, and build once a take is picked`);const Ui=I.includes(n)?await si(e,t,`--status ${n}`,ce):async()=>[],cn=I.includes(n)?Ns(e,t,{...k,status:Mi},n,ae,ee(e,xe)):null;if(a&&delete k.status,(n!==void 0||a)&&(k.status_at=new Date().toISOString()),s!==void 0&&(ii(k,s,await ni(e,t)),await di(e,t,k,k.next)),o&&(delete k.next,delete k.next_on_questions),n==="building"&&s===void 0&&k.next_on_questions&&(delete k.next,delete k.next_on_questions),(c!==void 0||d!==void 0||f!==void 0)&&!k.review_since&&(k.review_since=new Date().toISOString()),c!==void 0&&(Ds(k,c,ee(e,xe)),ht(k)),d!==void 0){we().find(E=>ve(E.dir,A(e)))?.remote||u("--pushed is for a board whose repo is a plain git remote; on GitHub, open a PR and pass --pr"),it(k)&&u(`${t} has a PR; its PR says when it merges`);try{ya(d)}catch{u("--pushed takes the name of the branch you pushed")}k.pushed={branch:d,at:new Date().toISOString()},k.branch=d,ht(k)}if(f!==void 0&&(k.merge_ready={branch:f,at:new Date().toISOString()},k.branch=f,ht(k)),I.includes(n)&&delete k.merge_ready,p!==void 0&&((typeof p!="string"||!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(p)||Number.isNaN(Date.parse(p)))&&u("--gh-since takes an ISO-8601 UTC time such as 2026-10-23T11:00:00Z"),k.gh_since=p),H&&(k.from_ask=H),w==="ready"&&e){const E=(await ke(e,t)).asked.filter(jt=>jt.state==="open").map(jt=>jt.id);E.length&&u(`${t} has ${E.length===1?"an open question":"open questions"} ${E.join(", ")}; it is ready once the owner has answered ${E.length===1?"it":"them"}`),await Rs(e,t,T.dir,k)}w!==void 0&&(k.state=w,delete k.unfiled),k.state==="ready"&&Ps.test(k.next??"")&&u(`${t} would read ready with Next "${k.next}", which asks for the state it is already in; pass --next with what happens next, or --clear-next`);const Ye=w==="ready"&&e&&k.implement_at?await Promise.all((await ze(e,C(t).wave,k)).map(E=>X(e,E))):null;Ye?.length&&delete k.implement_at,be&&(k.install=be),be===null&&delete k.install,!Wi&&!["building","fixing"].includes(n)&&Rt(k,rn)&&u(`that leaves ${t} In Progress with nothing building and no Next; pass --next "<what happens next>"`),(i!==void 0||H&&!k.ticket&&!k.linear?.length)&&await Yt(e,t,xe,k,i,ue,T.dir),await y(T.file,xe),St&&await St();const fe=await Ui();(P!==void 0||w==="ready"&&e)&&fe.push(...await Vt(e,t,"Recorded in the card's notes")),n==="building"&&fe.push(...await Vt(e,t,"Acted on: build started")),Ye&&fe.push(Ye.length?(await Ti(e,t,Ye)).line:`released: ${t} starts building now`),H&&fe.push(...(await re(e,t,{text:ae})).lines),cn&&e&&fe.push(...(await re(e,t,{text:cn,who:le})).lines);const dn=k.owner_step&&I.includes(n)?null:pa({status:n,pr:c??d??f}),ln=dn&&await wa(await bi(e),k,dn,{...ue,repo:ee(e,xe)});ln&&fe.push(ln),(c!==void 0||d!==void 0||f!==void 0||qi.includes(n))&&await Fi(T.dir,k.id,I.includes(n)),(c!==void 0||d!==void 0||f!==void 0||qi.includes(n))&&await mt(e,t);const _t=H?await _e(e):{};if(J(_t,H)!==void 0&&(delete _t[H],await y(he(e),{links:_t})),!I.includes(n)||on)return{file:T.file,lines:fe};const un=await As(e,t),Ki=new Date().toISOString();for(const E of un)await Y(U(e,E),{ts:Ki,who:"claude",text:`Unblocked: ${await X(e,t)} is ${n}.`}),await pe({dir:e},"card-unblocked",{key:E});return{file:T.file,lines:[...un.map(E=>`unblocked: ${E}`),...fe]}}r(pi,"changeItem");function wi(e,{size:t,model:i}){for(const[n,a,s]of[["size",t,["small","large"]],["model",i,["opus","sonnet"]]])a!==void 0&&(a===""?delete e[n]:e[n]=s.includes(a)?a:u(`--${n} takes ${s.join(" or ")}, or "" to clear it`))}r(wi,"setBuildChoice");function Es({held:e,unheld:t,pickUpAt:i}){return[e,t,i!==void 0].filter(Boolean).length>1&&u("pass one of --held, --unheld and --pick-up-at"),i!==void 0?i===""?null:{until:mi(i)}:e?{until:null}:t?null:void 0}r(Es,"holdOf");function mi(e){const t=typeof e=="string"&&ua.test(e)?Date.parse(e):NaN;return Number.isNaN(t)&&u('--pick-up-at takes an ISO time such as 2026-11-04T09:00, the Mac\u2019s local time unless it names an offset, or "" to resume the card'),t<=Date.now()&&u(`--pick-up-at ${e} has passed; pass a later time, or --unheld to resume the card now`),new Date(t).toISOString()}r(mi,"pickUpTime");const Ls=["building","fixing","checking",...I];function Cs(e){delete e.held,delete e.pick_up_at}r(Cs,"clearHold");function gt(e,t){t?e.held=!0:delete e.held,t?.until?e.pick_up_at=t.until:delete e.pick_up_at,!t&&e.status==="held"&&delete e.status,e.held_at=new Date().toISOString()}r(gt,"setHold");async function Er(e,t,i,{announce:n=!0}={}){const{source:a,wave:s,item:o}=await v(e,t);if(n){const c=new Date(i).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});await Y(U(e,t),{ts:new Date().toISOString(),who:"claude",text:`Resumed: its hold until ${c} ended.`}),(await ze(e,C(t).wave,o)).length||await pe({dir:e},"card-unblocked",{key:t,until:i})}gt(o,null),await y(a.file,s)}r(Er,"pickUp");async function hi(e,t,i){const n=String(i?.key||""),{wave:a,id:s}=C(n),{source:o,wave:c}=await Le(e,a),d={text:"Marked done.",who:"love"},f=c.items.find(m=>m.id===s)||u(`wave "${a}" has no item "${s}"`);if(!(t==="card-done"&&!!f.owner_step)&&["merged","dropped"].includes(f.status))return null;if(t==="card-done")return se(e,n,{status:f.status==="merged"?"merged":"done",force:!0,ownerDone:!0,...d});const l=i.hold?typeof i.until=="string"?new Date(i.until).toISOString():null:void 0;gt(f,i.hold?{until:l}:null),await y(o.file,c),i.hold&&(await Fi(o.dir,s,!1),await mt(e,n));const w=l&&new Date(l).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});return re(e,n,{text:i.hold?w?`Held until ${w}.`:"Held.":"Resumed.",who:"love"})}r(hi,"recordOwnerTap");function gi(e){try{return Ba(Ia(e))}catch(t){return u(t.message)}}r(gi,"checkSpec");async function Lr(e,t,i,n){const{item:a}=await v(e,t);return!a.owner_step||a.owner_check?.check!==i?null:se(e,t,{status:a.status==="merged"?"merged":"done",force:!0,ownerDone:!0,text:`Check passed: ${n}. Closed.`})}r(Lr,"passOwnerCheck");async function Cr(e,t,i,n){const{source:a,wave:s,item:o}=await v(e,t);return!o.owner_step||o.owner_check?.check!==i?!1:(o.owner_check.broken=n,await y(a.file,s),await re(e,t,{text:`The check for "${o.owner_step}" can't run (${n}), so it stopped; Claude is asked to fix it. Done still closes the card.`}),await pe({dir:e},"owner-check-broken",{key:t,check:i,error:n}),!0)}r(Cr,"breakOwnerCheck");async function $i(e,t,i){const n=[...new Set(String(i).split(",").map(a=>a.trim()).filter(Boolean))];for(const a of n)await v(e,W(t,a));return n}r($i,"depList");async function ki(e,t,i,n){if(!String(n).trim())return null;const a=W(t,String(n).trim());a===i&&u("--parent: a card can't be its own Parent");for(let s=a;s;){const o=s===a?await v(e,s):await v(e,s).catch(d=>{if(d instanceof Q)return null;throw d});if(!o)break;const{item:c}=o;c.parent&&W(C(s).wave,c.parent)===i&&u(`--parent: ${a} is a child of ${i}, so it can't be its Parent`),s=c.parent?W(C(s).wave,c.parent):null}return a}r(ki,"parentKey");async function $t(e,t,i,n){const a=t.split("/")[0],s=[...new Set(String(i).split(",").map(o=>o.trim()).filter(Boolean).map(o=>W(a,o)))];s.includes(t)&&u(`${n}: a card can't be related to itself`);for(const o of s)await v(e,o);return s}r($t,"relatedKeys");const yi=r((e,t,i)=>async()=>{for(const n of await q(e)){const a=await $(n.file,null);let s=!1;for(const o of Array.isArray(a?.items)?a.items:[]){const c=`${n.slug}/${o.id}`;c===t||!i(c)||!(o.related||[]).some(d=>W(n.slug,d)===t)||(o.related=o.related.filter(d=>W(n.slug,d)!==t),o.related.length||delete o.related,s=!0)}s&&await y(n.file,a)}},"dropLinksTo");async function Is(e,t,i,n){const a=await $t(e,t,n,"--related");return a.length?i.related=a:delete i.related,yi(e,t,s=>!a.includes(s))}r(Is,"setRelated");function Bs(e,t,i,n){const a=(i.related||[]).filter(s=>!n.includes(W(C(t).wave,s)));return a.length?i.related=a:delete i.related,yi(e,t,s=>n.includes(s))}r(Bs,"unrelateCards");const ee=r((e,t)=>e&&we().find(i=>ve(i.dir,e))?.github||t.repo,"boardRepoOf");async function Ir(e,t){const{wave:i,item:n}=await v(e,t);it(n)||u(`${t} has no PR`);const a=Ne(n,ee(e,i));a.length||u(`${t} has no repo`);const s=n.gh_since||"1970-01-01T00:00:00Z";try{const o=ge(await M(e)).github_login||await _a();return await Promise.all(a.map(async({repo:c,number:d})=>({repo:c,...$n(await gn({repo:c,pr:d,since:s}),{pr:d,since:s,me:o})})))}catch(o){u(o.message)}}r(Ir,"prReport");async function Br(e,t,{replyTo:i,body:n}){const a=i===void 0?"pr comment":"pr reply",s=yn(t)||u(`${a} takes a PR number or owner/repo#<n>`);i!==void 0&&!/^[1-9]\d*$/.test(i)&&u("pr reply takes the id of the review comment it answers");const o=j(n,"--body").trim();(o.startsWith(Te.trim())||o.includes(Ct))&&u(`${a} adds the ${Te.trim()} prefix and the claude-bridge marker itself; pass only your words`);const c=s.repo||ee(A(e),{})||u(`${a} needs a board on GitHub; this board has no repo there`);try{const d=await ja({repo:c,pr:s.number,replyTo:i,words:o,login:ge(await M(e)).github_login});return{lines:[`posted on PR ${s.repo?t:`#${t}`}: ${d}`]}}catch(d){u(d.message)}}r(Br,"postPR");const bi=r(async e=>Qn((await M(e)).tickets),"boardSource");async function Tr(e,t,{afterApproval:i,confirmed:n,tapped:a}={}){A(e);const{wave:s,item:o}=await v(e,t);it(o)||u(`${t} has no PR`),I.includes(o.status)&&u(`${t} is ${o.status}`);const c=ee(e,s),d=Ne(o,c);d.length||u(`${t} has no repo`);const f=new Date().toISOString();let p;try{p=await Promise.all(d.map(x=>gn({repo:x.repo,pr:x.number,since:f})))}catch(x){u(x.message)}const l=p.map(({view:x})=>x);if(kn(l)){const x=d.filter((O,R)=>l[R].state==="MERGED");return se(e,t,{status:"merged",text:`PR${x.length>1?"s":""} ${qe(x,c)} ${x.length>1?"were":"was"} already merged.`})}const w=d.map((x,O)=>({...x,...p[O]})).filter(({view:x})=>x.state==="OPEN"),m=Fe(s,(await M(e)).reviewers||[]).length>0,g=[];let b=!1,P=!1;for(const x of w){const{view:O,reviews:R}=x,{failing:Z,pending:ne}=$n(x,{pr:x.number,since:f,me:null}),F=Zi({...O,checks:{state:Z.length?"fail":ne?"pending":"pass",failing:Z,pending:ne}},{needsApproval:m}),B=R.filter(ie=>ie.state==="APPROVED").sort((ie,ce)=>String(ie.submitted_at).localeCompare(String(ce.submitted_at))).at(-1)?.commit_id;let oe=!!(B&&O.headRefOid&&B!==O.headRefOid);oe&&i===void 0&&zs(o,B,O.headRefOid)&&(oe=!1,P=!0),b||=oe,oe&&i===void 0&&F.push('commits after the approval: pass --after-approval "<why they are trivial>" when they are, else ask the approver to look again'),g.push(...d.length>1?F.map(ie=>`${qe([x],c)} ${ie}`):F)}w.length||g.push(d.length>1?"every PR is closed":"PR is closed"),i!==void 0&&!b&&u("--after-approval: no commit came after the approval; drop it");const D=o.manual_steps?.length?n===void 0?null:await ri(e,n,"--confirmed"):void 0;if(n!==void 0&&D===void 0&&u(`--confirmed: ${t} has no manual steps; drop it`),D===null&&g.push(`manual steps: merge only on the owner's word, with --confirmed "<their words>"`),g.length){const x=`Not merged: ${g.join("; ")}.`;a&&await re(e,t,{text:x}),u(x)}const S=[];for(const x of w){const O=await new Promise(R=>{Pt("gh",["pr","merge",String(x.number),"--repo",x.repo,"--squash"],{encoding:"utf8",timeout:12e4},(Z,ne,F)=>R(Z?String(F||Z.message).trim().split(`
17
- `)[0]:null))});O&&u(`gh pr merge ${qe([x],c)} failed: ${O}${S.length?` (merged ${qe(S,c)} before it)`:""}`),S.push(x)}const _=[fi(S,c),i!==void 0&&`After the approval: ${j(i,"--after-approval")}`,P&&`After the approval: only the board's rebase onto ${o.rebased.at(-1).base}.`,D&&`The owner's word on the manual steps: "${D}"`];return se(e,t,{status:"merged",text:_.filter(Boolean).join(" ")})}r(Tr,"mergeCard");async function qr(e,t){return(await v(e,t)).item.merge_ready||null}r(qr,"heldMerge");async function Fr(e,t){const{source:i,wave:n,item:a}=await v(e,t);return(!a.merge_ready||I.includes(a.status))&&u(`${t} isn't waiting for its Merge`),a.merge_ready.tapped=new Date().toISOString(),await y(i.file,n),a.merge_ready}r(Fr,"tapMerge");async function Mr(e,t){const{source:i,item:n}=await v(e,t);return(await Promise.all((await te(i.dir,n.id)).map(s=>$(s,null)))).filter(s=>s?.worktree?.path).sort((s,o)=>String(o.started).localeCompare(String(s.started)))[0]?.model}r(Mr,"builderModel");async function Yt(e,t,i,n,a,s,o){try{const c=await M(e),d=Et(c.tickets||{source:"internal"}),f=r((m,g)=>m.internal===!0?String(g.id).replace(/^S(\d+)$/,`${d.prefix}-$1`):null,"ownId");a===void 0&&d.source==="internal"&&Lt(d,f(i,n))&&(a=f(i,n));let p=0;if(d.source==="internal")for(const m of await q(e)){const g=await $(m.file,{});for(const b of g.items||[]){const P=[b.ticket,f(g,b)];if(p=Math.max(p,...P.map(D=>Lt(d,D)||0)),a!==void 0&&`${m.slug}/${b.id}`!==t&&P.includes(a))throw Object.assign(new Q(`${a} is already the ticket of ${m.slug}/${b.id}`),{held:!0})}}const l=a===void 0&&d.source!=="internal"&&n.plan?await N.readFile(await Ge(o,n.plan),"utf8"):n.notes||"",w=await fa(d,{ticket:a,title:n.title,body:l,repo:ee(e,i),next:p+1},s);n.ticket=w.ticket,w.created&&d.source!=="internal"?n.ticket_created=!0:delete n.ticket_created}catch(c){if(c instanceof Q)throw c;u(c.message)}}r(Yt,"assignTicket");const je=r((e,t)=>nt(h.join(e||rt(),"tickets"),t,{heartbeat:!0}),"ticketWrite"),se=r((e,t,i,n)=>i.ticket!==void 0||i.fromAsk!==void 0?je(e,()=>pi(e,t,i,n)):pi(e,t,i,n),"setItem"),Wr=r((e,t,i,n)=>je(e,()=>Ts(e,t,i,n)),"addItem");async function Ts(e,t,{title:i,notes:n,deps:a,notesFile:s,ticket:o,pickUpAt:c,parent:d,size:f,model:p,state:l,relate:w,intake:m=!1},g){const{wave:b,id:P}=t.includes("/")?C(t):{wave:t};P!==void 0&&me(P,"the item id"),l!==void 0&&!ci.includes(l)&&u("--state takes ready, grilling or prototyping");const{source:D,wave:S}=await Le(e,b);P!==void 0&&S.items.some(O=>O.id===P)&&u(`wave "${b}" already has an item "${P}"`);const _={id:P,title:j(i,"--title"),created:new Date().toISOString()};d!==void 0&&(_.parent=await ki(e,b,null,d)||u("--parent takes a card, such as <wave>/<id>")),n!==void 0&&(_.notes=j(n,"--notes")),wi(_,{size:f,model:p}),a!==void 0&&(_.deps=await $i(e,b,a)),s!==void 0&&(await Ge(D.dir,s),_.plan=h.normalize(s)),_.deps?.length&&(_.status="waiting"),c!==void 0&&gt(_,{until:mi(c)}),w!==void 0&&(_.related=await $t(e,t,w,"--relate")),l==="ready"&&li(P===void 0?"the card":t,_),l!==void 0?_.state=l:m&&e&&(_.unfiled=!0),await Yt(e,t,S,_,o,g,D.dir),P===void 0&&(_.id=ha(_.ticket),S.items.some(O=>O.id===_.id)&&u(`wave "${b}" already has an item "${_.id}"; link ${_.ticket} with item add ${b}/<id> --ticket ${_.ticket}`));const x=`${b}/${_.id}`;return S.items.push(_),await y(D.file,S),_.unfiled&&await pe(we().find(O=>ve(O.dir,e))||{dir:e},"card-added",{key:x,title:_.title}),{file:D.file,key:x,lines:[]}}r(Ts,"createItem");const Ur=r((e,{dryRun:t=!1}={},i)=>je(A(e),()=>qs(e,t,i)),"backfillTickets");async function qs(e,t,i){const n=await bi(e)||u("this board has no ticket source yet; pick one in Board settings first"),a=new Set,s=[];for(const p of await q(e)){const l=await $(p.file,null);if(Array.isArray(l?.items)){s.push({entry:p,wave:l});for(const w of l.items)w.ticket&&a.add(w.ticket),l.internal===!0&&n.source==="internal"&&a.add(String(w.id).replace(/^S(\d+)$/,`${n.prefix}-$1`))}}const o=[],c={linked:0,created:0,failed:0},d=r((p,l)=>l.ticket&&!(n.source!=="internal"&&p.internal===!0&&l.ticket===l.id),"ticketed");for(const{entry:p,wave:l}of s)for(const w of l.items){if(d(l,w)||I.includes(w.status))continue;const m=`${p.slug}/${w.id}`,g=[...ma(n,w.title),...(w.linear||[]).filter(b=>Lt(n,b))].find(b=>n.source!=="internal"||!a.has(b));if(t){c[g?"linked":"created"]++,o.push(`${m} would ${g?`link ${g}`:"get a new ticket"}`);continue}try{await Yt(e,m,l,w,g,i,p.dir),await y(p.file,l),a.add(w.ticket),c[g?"linked":"created"]++,o.push(`${m} ${g?"linked":"created"} ${w.ticket}`)}catch(b){if(!(b instanceof Q))throw b;c.failed++,o.push(`${m} failed: ${b.message}`)}}const f=t?`would link ${c.linked}, create ${c.created}`:`linked ${c.linked}, created ${c.created}, failed ${c.failed}`;return{lines:[...o,`backfill${t?" (dry run)":""}: ${f}`]}}r(qs,"backfill");const Kr=r(e=>je(e,async()=>{const t=[];for(const i of await q(e)){const n=await $(i.file,null);if(!Array.isArray(n?.items)||!Array.isArray(n.followups))continue;const a=n.followups.find(d=>n.items.some(f=>f.id===d.id));a&&u(`${i.slug}: follow-up ${a.id} has the id of a card; rename one, then restart the board`);const s=[];for(const{id:d,title:f,from:p,filed:l,done:w,...m}of n.followups){const g=w?e&&(await G(U(e,`${i.slug}/${d}`))).map(b=>b.ts).filter(Boolean).sort().pop()||l:null;s.push({id:d,title:f,...l?{created:l}:{},...p?{parent:W(i.slug,p)}:{},...w?{status:"done"}:{},...g?{status_at:g}:{},...m})}const{followups:o,...c}=n;await y(i.file,{...c,items:[...n.items,...s]}),t.push(`${i.slug}: ${s.length} follow-ups moved to cards (${s.filter(d=>d.status==="done").length} done)`)}return t}),"migrateFollowups"),Hr=r(e=>je(e,async()=>{const t=[];for(const i of await q(e)){const n=await $(i.file,null),a=(Array.isArray(n?.items)?n.items:[]).filter(s=>s.status==="planned");if(a.length){for(const s of a)delete s.status;await y(i.file,n),t.push(`${i.slug}: ${a.length} planned cards now have no status`)}}return t}),"dropPlannedStatus"),kt=r(e=>h.join(e,"data","done-archive.json"),"archiveFile"),Xt=r((e,t)=>nt(h.join(e,"done-archive"),t,{heartbeat:!0}),"archiveWrite"),yt=r(async e=>(await $(kt(e),{cards:[]})).cards||[],"readArchive"),bt=r(e=>`${e.closed_at||""}|${e.key}`,"archiveRank"),Fs=r((e,t)=>bt(e)<bt(t)?1:bt(e)>bt(t)?-1:0,"byClosed"),en=r(e=>h.join(e,"data","done-archive-count.json"),"archiveCountFile");async function Jr(e){const t=(await $(en(e),null))?.count;if(Number.isInteger(t))return t;const i=(await yt(e)).length;return await y(en(e),{count:i}),i}r(Jr,"archiveCount");async function tn(e,t){t.length?await y(kt(e),{cards:t.sort(Fs)}):await N.rm(kt(e),{force:!0}),await y(en(e),{count:t.length})}r(tn,"writeArchive");const zr=r((e,t)=>Xt(e,async()=>{const i=new Map(t.map(l=>[l.key,l])),n=new Date().toISOString(),a=[],s=[];for(const l of await q(e)){const w=await $(l.file,null);if(!Array.isArray(w?.items))continue;const m=w.items.filter(g=>i.has(`${l.slug}/${g.id}`)&&I.includes(g.status));if(m.length){for(const g of m)a.push({...i.get(`${l.slug}/${g.id}`),wave:l.slug,archived_at:n,item:g});s.push([l.file,{...w,items:w.items.filter(g=>!m.includes(g))}])}}if(!a.length)return[];const o=await $(L(e),{items:[]}),c=new Map(a.map(l=>[`item:${l.key}`,l]));for(const l of a)l.questions=(o.items||[]).filter(w=>w.topic===`item:${l.key}`&&w.resolved);const d=await _e(e);for(const l of a)l.asks=l.item.from_ask&&J(d,l.item.from_ask)===void 0?[l.item.from_ask]:[];const f=new Set(a.map(l=>l.key));await tn(e,[...(await yt(e)).filter(l=>!f.has(l.key)),...a.map(({key:l,wave:w,archived_at:m,closed_at:g,item:b,questions:P,asks:D})=>({key:l,wave:w,archived_at:m,closed_at:g,item:b,questions:P,asks:D}))]),a.some(l=>l.asks.length)&&await y(he(e),{links:{...d,...Object.fromEntries(a.flatMap(l=>l.asks.map(w=>[w,l.key])))}});for(const[l,w]of s)await y(l,w);const p=(o.items||[]).filter(l=>!(c.has(l.topic)&&l.resolved));return p.length!==(o.items||[]).length&&await y(L(e),{...o,items:p}),[...f]}),"archiveCards");async function Ms(e,t){return!e||!De(kt(e))?!1:Xt(e,async()=>{const i=await yt(e),n=i.find(l=>l.key===t);if(!n)return!1;const a=(await q(e)).find(l=>l.slug===n.wave),s=a&&await $(a.file,null);if(!Array.isArray(s?.items))return!1;s.items.some(l=>l.id===n.item.id)||await y(a.file,{...s,items:[...s.items,n.item]});const o=await $(L(e),{items:[]}),c=new Set((o.items||[]).map(l=>l.id)),d=(n.questions||[]).filter(l=>!c.has(l.id));d.length&&await y(L(e),{...o,items:[...o.items||[],...d]});const f=await _e(e),p=(n.asks||[]).filter(l=>J(f,l)===t);return p.length&&await y(he(e),{links:Object.fromEntries(Object.entries(f).filter(([l])=>!p.includes(l)))}),await tn(e,i.filter(l=>l!==n)),!0})}r(Ms,"restoreArchived");const xi=["push_tag","no_change"],vi=r(e=>Object.fromEntries(Object.entries(e).filter(([t])=>!xi.includes(t))),"stripQuestion"),Si=r(({summary:e,...t})=>t,"stripItem"),_i=r(e=>xi.some(t=>t in e),"holdsDead"),Gr=r(e=>Xt(e,async()=>{for(const n of await q(e)){const a=await $(n.file,null);Array.isArray(a?.items)&&a.items.some(s=>"summary"in s)&&await y(n.file,{...a,items:a.items.map(Si)})}const t=await $(L(e),null);(t?.items||[]).some(_i)&&await y(L(e),{...t,items:t.items.map(vi)});const i=await yt(e);i.some(n=>"card"in n||"summary"in n.item||(n.questions||[]).some(_i))&&await tn(e,i.map(({card:n,...a})=>({...a,...n?{closed_at:a.closed_at||n.sortAt}:{},item:Si(a.item),questions:(a.questions||[]).map(vi)})))}),"stripDeadFields"),ji=r(e=>`lineup-${e}.md`,"lineupNoteFile"),Ws=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:u(`a wave's slug uses only a-z, 0-9 and "-", not "${e}"`),"lineupSlug");async function xt(e,t){for(const i of await q(e)){const n=await $(i.file,null),a=(n?.lineups||[]).find(s=>s.slug===t);if(a)return{source:i,wave:n,lineup:a}}return u(`this board has no wave "${t}"; lineup create makes one`)}r(xt,"findLineup");async function Us(e,t){(await M(e)).items!=="waves"&&u(`topic wave:${t} is for a board over ~/.claude/waves; a lineup's triage uses lineup:<slug>`),await Le(e,t)}r(Us,"triagedWave");function Qe(e,t,i,n){return i.map(a=>{const{wave:s,id:o}=C(a);s!==e.slug&&u(`${a} is not in wave file "${e.slug}"; a wave's cards share one file`),t.items.some(d=>d.id===o)||u(`wave "${s}" has no item "${o}"`);const c=(t.lineups||[]).find(d=>d.slug!==n&&!d.finished_at&&At(d).flat().includes(o));return c&&u(`${a} is already in the wave "${c.slug}"; lineup remove ${c.slug} ${a} first`),o})}r(Qe,"lineupCards");function Oi(e){const t=[e??[]].flat().map(i=>String(i).split(",").map(n=>n.trim()).filter(Boolean));return(!t.length||t.some(i=>!i.length))&&u("--phase takes one phase's cards, as <wave>/<id>,<wave>/<id>, once per phase in build order"),t}r(Oi,"phaseKeys");function Pi(e,t,i,n){const a=new Set(i.map(s=>`${e.slug}/${s}`));for(const s of i){const o=t.items.find(f=>f.id===s),c=(o.deps||[]).filter(f=>!a.has(W(e.slug,f))),d=n.findIndex(f=>f.includes(s));d>0&&c.push(...n[d-1]),c.length?o.deps=c:delete o.deps}}r(Pi,"chain");async function Di(e,t,i,n,a){a!==void 0&&await ct(h.join(t.dir,ji(n.slug)),`${j(a,"the note").replace(/\n*$/,`
18
- `)}`),await y(t.file,i);const{tickets:s}=ge(await M(e));return{file:t.file,lines:[`order: ${Ai(i,n.phases,s)}`]}}r(Di,"writeLineup");const Ai=r((e,t,i)=>t.map(n=>n.map(a=>tt(e,e.items.find(s=>s.id===a)||{id:a},i)).join(", ")).join(" \u2192 "),"orderLine");async function Qr(e,t,{title:i,goal:n,phases:a,note:s}){Ws(t);const o=Oi(a);s===void 0&&u("lineup create needs the wave note, as --note <text> or --file <path>");const c=await q(e);c.some(w=>w.slug===t)&&u(`"${t}" is a wave file on this board; pick another slug`);for(const w of c)((await $(w.file,null))?.lineups||[]).some(m=>m.slug===t)&&u(`this board already has a wave "${t}"`);const d=c.find(w=>w.slug===C(o[0][0]).wave)||u(`this board has no wave "${C(o[0][0]).wave}"`),{wave:f}=await Le(e,d.slug),p=o.map(w=>Qe(d,f,w,t));new Set(p.flat()).size!==p.flat().length&&u("--phase names a card twice");const l={slug:t,title:j(i,"--title"),goal:j(n,"--goal"),phases:p,created:new Date().toISOString()};return Pi(d,f,p.flat(),p),f.lineups=[...f.lineups||[],l],Di(e,d,f,l,s)}r(Qr,"createLineup");async function Zr(e,t,{add:i,after:n,remove:a,phases:s,title:o,goal:c,note:d}){[i,a,s,o,c,d].every(m=>m===void 0)&&u("nothing to change: pass --add, --remove, --phase, --title, --goal, --note or --file"),n!==void 0&&i===void 0&&u("--after goes with --add");const{source:f,wave:p,lineup:l}=await xt(e,t);l.finished_at&&u(`the wave "${t}" is finished`),l.phases=At(l),delete l.items;const w=l.phases.flat();if(i!==void 0){const[m]=Qe(f,p,[i],t);w.includes(m)&&u(`${i} is already in the wave "${t}"`);const g=n===void 0?l.phases.length:l.phases.findIndex(b=>b.includes(Qe(f,p,[n],t)[0]))+1;g===0&&u(`--after ${n} is not in the wave "${t}"`),l.phases.splice(g,0,[m])}if(a!==void 0){const[m]=Qe(f,p,[a],t);w.includes(m)||u(`${a} is not in the wave "${t}"`),w.length===1&&u(`${a} is the wave's last card; lineup finish ${t} ends the wave`),l.phases=l.phases.map(g=>g.filter(b=>b!==m)).filter(g=>g.length)}if(s!==void 0){const m=Oi(s).map(b=>Qe(f,p,b,t)),g=m.flat();(g.length!==w.length||new Set(g).size!==g.length||g.some(b=>!w.includes(b)))&&u("--phase lists each of the wave's cards once"),l.phases=m}return o!==void 0&&(l.title=j(o,"--title")),c!==void 0&&(l.goal=j(c,"--goal")),Pi(f,p,[...new Set([...w,...l.phases.flat()])],l.phases),Di(e,f,p,l,d)}r(Zr,"changeLineup");async function Vr(e,t){const{source:i,wave:n,lineup:a}=await xt(e,t);return a.finished_at=new Date().toISOString(),await y(i.file,n),{file:i.file}}r(Vr,"finishLineup");async function Yr(e,t,i){const{source:n,wave:a,lineup:s}=await xt(e,t),o=await N.readFile(h.join(n.dir,ji(t)),"utf8").catch(()=>"");return{title:s.title,markdown:`**Goal** ${s.goal}
14
+ ${fn}`,login:l});return await _a(e,i,{state:"posted",text:o,review_url:w}),{lines:[`posted review on PR #${t}: ${w}`]}}catch(u){if(u instanceof ae)throw u;d(u.message)}}r(sc,"postIncomingReview");const Et=r(e=>g.join(e,"data","handshake.json"),"handshakeFile");async function Sa(e){const t=await $(Et(e),null);if(!t?.hello)return t;const{hello:n,...a}=t;return{...a,ping:a.ping||n}}r(Sa,"readHandshake");async function rc(e,t){A(e),ge(t,"the event id");const n=(await K(g.join(e,"data","events.jsonl"))).find(i=>i.id===t);n?.kind!=="ping"&&n?.kind!=="hello"&&d(`no ping event "${t}" in this site's data/events.jsonl`);const a=await Sa(e)||{};return a.ping?.id&&a.ping.id!==t&&d(`"${t}" is an older ping; ack the newest, "${a.ping.id}"`),await y(Et(e),{...a,ping:a.ping||{id:t,at:n.ts,session:null},ack:{id:t,at:new Date().toISOString()}}),Et(e)}r(rc,"ackPing");async function cc(e){const t=await Sa(e);t&&!t.done_at&&await y(Et(e),{...t,done_at:new Date().toISOString()})}r(cc,"finishHandshake");const kn={name:/^[^\n]{1,40}$/,slack_user:/^U[A-Z0-9]+$/,dm:/^D[A-Z0-9]+$/,github:/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/},lt=r((e,t)=>e.trim().toLowerCase()===t.trim().toLowerCase(),"sameName");function bn(e,t){return Array.isArray(e)||d(`${t} must be a JSON array of reviewers`),e.forEach((n,a)=>{const i=`${t}[${a}]`;Ue(n,Object.keys(kn),i);for(const[o,s]of Object.entries(kn))(typeof n[o]!="string"||!s.test(n[o].trim()))&&d(`${i}.${o} must match ${s}`);e.slice(0,a).some(o=>lt(o.name,n.name))&&d(`${t} names "${n.name}" twice`)}),e.map(n=>Object.fromEntries(Object.keys(kn).map(a=>[a,n[a].trim()])))}r(bn,"checkReviewers");async function yn(e,t){const{file:n,reviewers:a}=await ut(e,o=>{const s=bn(t(o.reviewers||[]),"the reviewers");return{set:{reviewers:s},reviewers:s}}),i=[];for(const o of await F(e)){const s=await $(o.file,null);(s?.items||[]).some(c=>!G(c))&&(await y(o.file,{...s,reviewers:a}),i.push(`wave ${o.slug}: reviewers copied`))}return{file:n,lines:i}}r(yn,"writeReviewers");const lc=r((e,t)=>yn(e,()=>bn(t,"the reviewer file")),"setReviewers"),ze=r(e=>g.join(A(e),"data","pr-slack.json"),"prSlackFile"),It=r(e=>$(ze(e),{}),"readPRSlack"),dt=r(e=>!!(e?.ts||e?.posting_at),"held");async function Le(e,t){return at(e,async()=>{const n=JSON.parse(await P.readFile(e,"utf8").catch(()=>"{}")),a=t(n);return await rt(e,JSON.stringify(n,null,2)+`
15
+ `),a})}r(Le,"editJSON");async function Lt(e,t,n){await Le(e,i=>{const o=t(i);dt(o)&&d("that Slack message has already gone out"),o.posting_at=new Date().toISOString()});let a;try{a=await n()}catch(i){throw await Le(e,o=>{delete t(o).posting_at}),i}try{await Le(e,i=>{const o=t(i);o.ts=a,delete o.posting_at})}catch(i){d(`posted, but its record failed, so it won't go again: ${i.message}`)}return a}r(Lt,"postOnce");const dc=r((e,t,n,a)=>Lt(ze(e),i=>{const o=i[t.pr]||={head:t.head,links:{},replies:[]};return o.head??=t.head,Object.assign(o.links[t.reviewer]||={reactions:[]},{channel:n})},()=>a(n)),"postLink"),uc=r((e,t,n)=>Lt(ze(e),a=>a[t.pr].replies.find(i=>i.id===t.id).to[t.reviewer],n),"postReply"),fc=r((e,t)=>Le(ze(e),n=>{const a=n[t.pr].links[t.reviewer];a.reactions.includes(t.emoji)||a.reactions.push(t.emoji)}),"recordReaction"),ja=r((e,t,n,a)=>Le(ze(e),i=>{const o=i[t],s=Object.keys(o?.links||{}).filter(c=>o.links[c].ts);return s.length?(o.replies.push({id:`${n}-${o.replies.length+1}`,text:a,to:Object.fromEntries(s.map(c=>[c,{}]))}),s):[]}),"queueReply");function ns(e,t,n){const a=e.commits||[],i=a.findIndex(o=>o.oid===n);return[`New push to #${t}`,a.slice(i+1).map(o=>o.subject).join(" \xB7 ")].filter(Boolean).join(`
16
+ `)}r(ns,"pushText");async function pc(e){const t=await $(rn,{}),n=r((a,i)=>{const o=V(t.prs,a)?.head;return!!o&&o!==i.head&&Object.values(i.links).some(s=>s.ts)},"moved");return Object.entries(await It(e)).some(([a,i])=>n(a,i))?Le(ze(e),a=>{const i=[];for(const[o,s]of Object.entries(a)){if(!n(o,s))continue;const c=V(t.prs,o),l=Object.keys(s.links).filter(u=>s.links[u].ts);s.head&&(s.replies.push({id:`push-${s.replies.length+1}`,text:ns(c,o.split("#")[1],s.head),to:Object.fromEntries(l.map(u=>[u,{}]))}),i.push(o)),s.head=c.head}return i}):[]}r(pc,"queuePushes");function as(e,t,n){const a=Number.isInteger(e.additions)?`+${e.additions}/-${e.deletions}, ${e.files} file${e.files===1?"":"s"}`:null;return[e.title||n.title,wn(t),a].filter(Boolean).join(`
17
+ `)}r(as,"linkText");const Oa=r(e=>!!e?.approved||e?.reviewDecision==="APPROVED","approved");async function wc(e,t){A(e);const n=await ra.waiting("slack");if(n)return{waiting:n,links:[],replies:[],reactions:[],digest:null};const a=await ee(e),i=await $(rn,{}),o=await It(e),s=[];for(const w of await F(e)){const p=await $(w.file,{}),f=mn(p,a.reviewers);for(const m of p.items||[])if(m.lane!=="done")for(const h of De(m,te(e,p))){const k=ot(h),v=V(i.prs,k);if(!(v?.state!=="OPEN"||v.isDraft))for(const E of f)dt(o[k]?.links?.[E.name])||s.some(O=>O.pr===k&&O.reviewer===E.name)||s.push({pr:k,key:`${w.slug}/${m.id}`,reviewer:E.name,slack_user:E.slack_user,head:v.head||null,text:as(v,h,m)})}}const c=[],l=[];for(const[w,p]of Object.entries(o)){const f=V(i.prs,w),m=[Oa(f)&&"git_approved",f?.state==="MERGED"?"git_merged":f?.state==="CLOSED"&&"x"].filter(Boolean);for(const[h,k]of Object.entries(p.links))if(k.ts)for(const v of m)k.reactions.includes(v)||l.push({pr:w,reviewer:h,channel:k.channel,ts:k.ts,emoji:v});for(const h of p.replies)for(const[k,v]of Object.entries(h.to))dt(v)||c.push({pr:w,id:h.id,reviewer:k,channel:p.links[k].channel,thread_ts:p.links[k].ts,text:h.text})}const u=await is(e,t);return{links:s,replies:c,reactions:l,digest:u.due?u:null}}r(wc,"reviewDue");async function mc(e,t,n){A(e);const a=j(n,"what changed"),{wave:i,item:o}=await x(e,t),s=[];for(const l of De(o,te(e,i)))s.push(...await ja(e,ot(l),"updated",`Updated: ${a}`));const c=[...new Set(s)];return{lines:[c.length?`Updated queued for ${c.join(", ")} on ${t}`:`no reviewer has a link to ${t}'s PRs; nothing queued`]}}r(mc,"reviewUpdated");async function hc(e,t){const{wave:n,item:a}=await x(e,t),i=await It(e);return De(a,te(e,n)).some(o=>Object.values(i[ot(o)]?.links||{}).some(s=>s.ts))}r(hc,"cardLinked");const Pa=r(e=>g.join(A(e),"data","review-digests.json"),"digestFile");async function is(e,{now:t=new Date}={}){A(e);const n=await ee(e),a=await $(rn,{}),i=await It(e),o=_o(),s=new Map;for(const f of await F(e)){const m=await $(f.file,{}),h=mn(m,n.reviewers);for(const k of h)s.has(k.name)||s.set(k.name,{name:k.name,slack_user:k.slack_user,dm:k.dm,items:[]});for(const k of m.items||[]){if(k.lane==="done")continue;const v=`${f.slug}/${k.id}`,E=vo(m,k,v,o);for(const O of De(k,te(e,m))){const B=ot(O),S=V(a.prs,B);if(!(S?.state!=="OPEN"||S.isDraft||S.failing?.length||Oa(S)))for(const _ of h){const N=i[B]?.links?.[_.name]?.ts||V(E,_.name)?.ts||null,T=N?(t.getTime()-Number(N)*1e3)/864e5:null,C=T>2?Math.floor(T):null;s.get(_.name).items.push({key:v,pr:B,title:k.title,url:wn(O),sent_ts:N,...C?{days_old:C}:{},text:`${wn(O)} \xB7 ${k.title}${C?` (${C} days old)`:""}`})}}}}const{date:c,open:l}=So(t),u=((await $(Pa(e),{})).digests||[]).filter(f=>f.date===c),w=await ra.waiting("slack");for(const f of u)s.has(f.reviewer)||s.set(f.reviewer,{name:f.reviewer,items:[]});const p=[...s.values()].map(f=>{const m=u.find(k=>k.reviewer===f.name),h=m?!m.posting_at&&(!m.ts||m.replies.some(k=>!dt(k))):f.items.length>0;return{...f,owed:h,...m?{sent:m}:{}}}).filter(f=>f.items.length||f.sent);return{date:c,due:!w&&l&&p.some(f=>f.owed),...w?{waiting:w}:{},reviewers:p}}r(is,"reviewDigest");async function gc(e,t,n,{channel:a,header:i,post:o}){const s=Pa(e),c=r(w=>(w.digests||[]).find(p=>p.date===t&&p.reviewer===n.name),"entry");await Le(s,w=>{c(w)||(w.digests=[...(w.digests||[]).filter(p=>p.date===t),{date:t,reviewer:n.name,channel:a,replies:n.items.map(p=>({pr:p.pr,text:p.text}))}],delete w.last_date)});const l=c(await $(s,{})),u=l.ts||await Lt(s,c,()=>o({channel:l.channel,text:i(l.replies.length)}));for(const[w,p]of l.replies.entries())dt(p)||await Lt(s,f=>c(f).replies[w],()=>o({channel:l.channel,thread_ts:u,text:p.text}))}r(gc,"postDigest");function $c(e,t){const n=bn(Array.isArray(t)?t:[t],"the reviewer file");return yn(e,a=>[...a.map(i=>n.find(o=>lt(o.name,i.name))||i),...n.filter(i=>!a.some(o=>lt(i.name,o.name)))])}r($c,"addReviewers");function kc(e,t){return yn(e,n=>{const a=t.filter(i=>!n.some(o=>lt(o.name,i)));return a.length&&d(`this board has no reviewer ${a.map(i=>`"${i}"`).join(", ")}`),n.filter(i=>!t.some(o=>lt(i.name,o)))})}r(kc,"removeReviewers");const os=r(()=>g.join(tn.homedir(),".claude","kickflip","settings.json"),"hubSettingsFile"),xn=r(e=>{try{return e()}catch(t){throw t instanceof Ao&&d(t.message),t}},"ruleCall"),vn=r(async e=>la(await $(g.join(e,"board.json"),{})),"readBoardRules");let Aa=Promise.resolve();function ut(e,t){const n=Aa.then(async()=>{const a=g.join(A(e),"board.json"),i=await $(a,{}),{set:o,...s}=t(i);return await y(a,{...i,...o}),{file:a,...s}});return Aa=n.catch(()=>{}),n}r(ut,"writeBoardJSON");const Da=r((e,t)=>ut(e,n=>{const a=xn(()=>t(la(n)));return{set:{rules:a.rules},...a}}),"writeBoardRules");function bc(e,t){const n=Object.keys(t||{}).sort().join(),a=n==="level,login",i=a?t.login:t?.[n],o=typeof i=="string"?i.trim():null;(!["add","remove","level,login"].includes(n)||!Bo(o))&&d("Send add or remove with one login, like name@github, or a listed login with its level."),a&&!["view","full"].includes(t.level)&&d("A level is view or full.");const s=r(({login:c,level:l})=>l==="full"?{login:c,level:l}:c,"stored");return ut(e,c=>{const l=Lo(c);a&&!l.some(p=>p.login===o)&&d(`${o} isn't shared with yet. Add them first.`);const w=(n==="remove"?l.filter(p=>p.login!==o):a?l.map(p=>p.login===o?{login:o,level:t.level}:p):l.some(p=>p.login===o)?l:[...l,{login:o,level:"view"}]).map(s);return{set:{share:w},share:w}}).then(({share:c})=>({share:c}))}r(bc,"saveBoardShare");const Ra=r(e=>typeof e=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e),"validGithubLogin"),Na=r(e=>{try{return e?dn(e):null}catch{return null}},"storedTickets"),_n=r(e=>Number.isInteger(e)&&e>=1&&e<=99,"validCap"),Sn=["max_implementers","max_planners"],$e=r((e,t)=>{const n=Na(e?.tickets);return{auto_merge:e?.auto_merge===!0,auto_build:e?.auto_build===!0,auto_pull:e?.auto_pull===!0,max_implementers:_n(e?.max_implementers)?e.max_implementers:Mi,max_planners:_n(e?.max_planners)?e.max_planners:Wi,auto_triage:e?.auto_triage===!0,auto_review:e?.auto_review===!0,idle_notification:e?.idle_notification===!0,skip_draft:e?.skip_draft===!0,prs:t?.hub?e?.prs===!0:e?.prs!==!1,...n?{tickets:n}:{},...Ra(e?.github_login)?{github_login:e.github_login}:{},...ca(e?.build_target)?{build_target:e.build_target}:{}}},"settingsOf");async function yc(e,t,n=new Date){let a;try{a=eo(t,n)}catch(i){d(i.message)}return await ut(e,()=>({set:{paused_until:a??void 0}})),a}r(yc,"setBoardPause");async function ss(e){const t=A(e);return $e(await $(g.join(t,"board.json"),{}),fe().find(n=>he(n.dir,t)))}r(ss,"readBoardSettings");const xc=r((e,t)=>!!e?.github&&(!e.local||e.hub===!0)&&t.prs,"opensPRs");async function Ke(e,t){Ue(t,["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","tickets","github_login","build_target"],"the settings"),Object.keys(t).length||d("the settings name nothing to change"),t={...t};for(const[n,a]of Object.entries(t))if(n==="tickets"){try{t.tickets=dn(a)}catch(o){d(o.message)}t.tickets.source==="linear"&&!fo()&&d(lo);const i=t.tickets.source==="github"&&e?fe().find(o=>he(o.dir,e)):null;i&&!i.github&&d("GitHub issues need the board\u2019s repo on GitHub, and this board\u2019s repo isn\u2019t; pick internal or linear")}else n==="github_login"?a===""||a===null?t.github_login=void 0:Ra(a)||d("github_login must be a GitHub login"):n==="build_target"?a===null?t.build_target=void 0:ca(a)||d('build_target must name .kickflip/build-<target>: letters, digits, ".", "_" or "-", or null for .kickflip/build'):Sn.includes(n)?_n(a)||d(`${n} must be a whole number from 1 to 99`):Dt(a,n);if(t.prs===!1){const n=e?fe().find(a=>he(a.dir,e)):null;n?.github&&!n.local&&!(await jo(n.owner?.repo)).pushMain&&d("Merge lands each card on main and pushes it, so turn on Push main after a land first")}return ut(e,n=>{const a=e?fe().find(s=>he(s.dir,e)):null,i=$e(n,a),o=$e({...n,...t},a);return{set:t,settings:o,changed:Object.keys(t).filter(s=>JSON.stringify(i[s])!==JSON.stringify(o[s]))}})}r(Ke,"saveBoardSettings");async function vc(){if(uo().set)return[];const e=[];for(const t of fe()){if((await ss(t.dir)).tickets?.source!=="linear")continue;const{settings:n}=await Ke(t.dir,{tickets:{source:"internal"}});await se(t,"settings",{tickets:n.tickets}),e.push(`/${t.route} fell back to internal tickets: Hub has no Linear key`)}return e}r(vc,"fallBackFromLinear");async function _c(e,t,n){const a=String(t).replace(/-/g,"_"),i=["auto_merge","auto_build","auto_pull","max_implementers","max_planners","auto_triage","auto_review","idle_notification","skip_draft","prs","github_login","tickets","build_target"];if(i.includes(a)||d(`no board setting "${t}"; settings are ${i.map(c=>c.replace(/_/g,"-")).join(", ")}`),a==="tickets"){let c;try{c=JSON.parse(n)}catch{d('settings set tickets takes the source as JSON, such as {"source":"github"}')}const{file:l,settings:u}=await Ke(e,{tickets:c});return{file:l,lines:rs({tickets:u.tickets})}}if(a==="github_login"){const{file:c,settings:l}=await Ke(e,{github_login:n});return{file:c,lines:[`github-login ${l.github_login||"unset"}`]}}if(a==="build_target"){const{file:c,settings:l}=await Ke(e,{build_target:n==="default"?null:n});return{file:c,lines:[`build-target ${l.build_target||"default"}`]}}if(Sn.includes(a)){const{file:c,settings:l}=await Ke(e,{[a]:/^\d+$/.test(n)?Number(n):n});return{file:c,lines:[`${t} ${l[a]}`]}}["on","off"].includes(n)||d(`settings set ${t} takes on or off, not "${n}"`);const{file:o,settings:s}=await Ke(e,{[a]:n==="on"});return{file:o,lines:[`${t} ${s[a]?"on":"off"}`]}}r(_c,"setBoardSetting");const rs=r(e=>Object.entries(e).map(([t,n])=>`setting ${t.replace(/_/g,"-")} ${t==="tickets"?JSON.stringify(n):t==="github_login"||Sn.includes(t)?n:t==="build_target"?n||"default":n?"on":"off"}`),"settingsLines");async function jn(e,t,n){const a=t?xn(()=>n(Pt())):await Da(e,n);return t&&xn(()=>Io(a.rules)),{file:t?os():a.file,lines:[`id ${a.rule.id}`]}}r(jn,"changeRules");async function Ea(e,t,n){const a=r(s=>s.map(c=>c.id),"ids");n||A(e);const[i,o]=n?[a(Pt()),e?a(await vn(e)):[]]:[a(await vn(e)),a(Pt())];i.includes(t)||d(`no ${n?"hub":"board"} rule "${t}"${o.includes(t)?`; it is a ${n?"board":"hub"} rule, so ${n?"drop":"pass"} --hub`:""}`)}r(Ea,"checkRuleId");async function Sc(e){const t=r(n=>a=>`${n} ${a.id} ${a.by==="claude"?"claude":"owner"} ${a.text}`,"line");return{lines:[...Pt().map(t("hub")),...e?(await vn(e)).map(t("board")):[]]}}r(Sc,"listRules");function jc(e,{text:t,hub:n=!1,by:a}){return n||A(e),jn(e,n,i=>Do(i,{text:t,by:a}))}r(jc,"addRuleTo");async function Oc(e,t,{text:n,hub:a=!1}){return await Ea(e,t,a),jn(e,a,i=>Ro(i,t,n))}r(Oc,"editRuleIn");async function Pc(e,t,{hub:n=!1}){return await Ea(e,t,n),jn(e,n,a=>No(a,t))}r(Pc,"removeRuleFrom");const Ac=r((e,t,n)=>Da(e,a=>Eo(a,t,n)),"saveBoardRules");async function Dc(e,{id:t,title:n,summary:a}){A(e);const i=await $(I(e),{topics:[],items:[]}),o=i.topics||[];return o.some(s=>s.id===j(t,"the topic id"))&&d(`topic "${t}" already exists`),o.push({id:t.trim(),title:j(n,"--title"),...a===void 0?{}:{summary:j(a,"--summary")}}),await y(I(e),{...i,topics:o,items:i.items||[]}),I(e)}r(Dc,"addTopic");const Ia=["quiet","info","notice","danger","success"];function cs(e,t){if(e.diagram===void 0!=(e.practice===void 0)&&d(`${t} needs diagram and practice together`),e.diagram===void 0)return;const n=e.diagram;(!Array.isArray(n)||!n.length)&&d(`${t}.diagram must be a non-empty array of rows`),n.forEach((a,i)=>{(!Array.isArray(a)||!a.length||a.length>4)&&d(`${t}.diagram[${i}] must be a row of 1 to 4 boxes`),a.forEach((o,s)=>{const c=`${t}.diagram[${i}][${s}]`;Ue(o,["text","tone","to"],c),j(o.text,`${c}.text`),X(o.tone,l=>Ia.includes(l)?l:d(`${c}.tone takes ${Ia.join(", ")}`)),X(o.to,l=>Array.isArray(l)&&i+1<n.length&&l.every(u=>Number.isInteger(u)&&u>=0&&u<n[i+1].length)?l:d(`${c}.to must list boxes of the next row by index`))})}),(!Array.isArray(e.practice)||!e.practice.length)&&d(`${t}.practice must be a non-empty array of lines`),e.practice.forEach((a,i)=>j(a,`${t}.practice[${i}]`))}r(cs,"checkDiagram");function ls(e,t){(!Array.isArray(e)||!e.length)&&d(`${t}.options must be a non-empty array`);const n=new Set;e.forEach((a,i)=>{const o=`${t}.options[${i}]`;Ue(a,["id","label","recommended","detail","diagram","practice"],o),cs(a,o),n.has(j(a.id,`${o}.id`))&&d(`${t} has two options with id "${a.id}"`),n.add(a.id),j(a.label,`${o}.label`).startsWith(`${a.id}. `)&&d(`${o}.label starts with "${a.id}. ", but the page adds the option letter itself`),X(a.recommended,s=>Dt(s,`${o}.recommended`)),X(a.detail,s=>typeof s=="string"?s:d(`${o}.detail must be text`))}),e.filter(a=>a.recommended).length>1&&d(`${t} recommends more than one option`)}r(ls,"checkOptions");function ds(e,t,n){Ue(e,["option","card","answer"],n),t.some(a=>a.id===j(e.option,`${n}.option`))||d(`${n}.option names no option "${e.option}"`),j(e.card,`${n}.card`),j(e.answer,`${n}.answer`)}r(ds,"checkSuggested");const Je=r(e=>g.join(e,"data","prototypes.json"),"prototypesFile"),On=r((e,t,n)=>g.join(e,"public","prototypes",t,`${n}.js`),"takeFile");async function ft(e){const t=e?(await $(Je(e),{})).prototypes:null;return Array.isArray(t)?t:[]}r(ft,"readPrototypes");const us=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:d(`a prototype topic uses only a-z, 0-9 and "-", not "${e}"`),"prototypeTopic");function fs(e){const t=/^([^:]*):(.*)$/s.exec(e);(!t||!/^[A-Z]$/.test(t[1]))&&d(`--take takes KEY:"Label" with KEY one capital letter, not "${e}"`);const n=j(t[2],`--take ${t[1]}'s label`);return/^[A-Z]\.\s/.test(n)&&d(`--take ${t[1]}'s label starts with "${n.slice(0,3)}"; the take pill shows the label alone, so drop the letter`),{key:t[1],label:n}}r(fs,"prototypeTake");function ps(e){const t=/^([^:]*):(.*)$/s.exec(e);return(!t||!/^[a-z0-9][a-z0-9-]*$/.test(t[1]))&&d(`--state takes KEY:"Label" with KEY in a-z, 0-9 and "-", not "${e}"`),{key:t[1],label:j(t[2],`--state ${t[1]}'s label`)}}r(ps,"prototypeState");const La=r(e=>e.find((t,n)=>e.findIndex(a=>a.key===t.key)!==n),"twiceIn"),Se=r(e=>/^prototype-(.+)\.html$/.exec(g.posix.basename(String(e||"").split(/[?#]/)[0]))?.[1],"pageTopic");async function Rc(e,{topic:t,title:n,card:a,takes:i=[],served:o,states:s=[]}){A(e),us(t),j(n,"--title"),i.length||d('prototype add needs a --take KEY:"Label" for each take');const c=i.map(fs),l=La(c);l&&d(`--take ${l.key} is given twice`),o!==void 0&&!/^\/\S*$/.test(o)&&d(`--served takes the page's path on the served site, starting with "/", not "${o}"`),s.length&&o===void 0&&d("--state goes with --served: only a served round re-renders its states in place");const u=s.map(ps),w=La(u);w&&d(`--state ${w.key} is given twice`),a!==void 0&&await x(e,a);const p=await ft(e);p.some(h=>h.topic===t)&&d(`prototype ${t} is already recorded; run prototype rm ${t} first, or pick another topic`),await P.stat(g.join(e,"public",`prototype-${t}.html`)).then(()=>!0,()=>!1)&&d(`public/prototype-${t}.html is a hand-written page that questions may link; pick another topic`);const f={topic:t,title:n.trim(),...a===void 0?{}:{card:a},...o===void 0?{}:{served:o},takes:c,...u.length?{states:u}:{},at:new Date().toISOString()};await y(Je(e),{prototypes:[...p,f]});const m=`page prototype-${t}.html`;return o!==void 0?{file:Je(e),lines:[m,`served ${o} from the base prototype worktree (board.mjs prototype base)`,...u.length?[`states ${u.map(h=>h.key).join(", ")}`]:[]]}:{file:Je(e),lines:[m,...c.map(h=>`take ${h.key} ${On(e,t,h.key)}`)]}}r(Rc,"addPrototype");async function Nc(e){A(e);const t=await ft(e);return{lines:t.length?t.map(n=>[n.topic,n.title,n.takes.map(a=>`${a.key}: ${a.label}`).join(", "),n.card,n.served&&`served ${n.served}`,n.states&&`states ${n.states.map(a=>a.key).join(", ")}`].filter(Boolean).join(" \xB7 ")):["no prototypes recorded"]}}r(Nc,"listPrototypes");async function Ec(e){const t=(await ft(e)).filter(a=>a.served).map(a=>a.topic);if(!t.length)return[];const n=await $(I(e),{items:[]});return t.filter(a=>(n.items||[]).some(i=>!i.resolved&&Se(i.url)===a))}r(Ec,"servedRoundsOpen");async function Ic(e,t){A(e);const n=await ft(e);n.some(o=>o.topic===t)||d(`no prototype "${t}" is recorded`);const i=((await $(I(e),{items:[]})).items||[]).filter(o=>!o.resolved&&Se(o.url)===t).map(o=>o.id);return i.length&&d(`prototype ${t} backs open question ${i.join(", ")}; resolve ${i.length===1?"it":"them"} first`),await y(Je(e),{prototypes:n.filter(o=>o.topic!==t)}),Je(e)}r(Ic,"removePrototype");async function ws(e,t,n){const a=t.url?.split(/[?#]/)[0];if(!a||!g.posix.basename(a).startsWith("prototype-"))return;const i=g.posix.basename(a),o=t.url.split("#")[0].split("?").slice(1).join("?"),s=`${n}.url`;new URLSearchParams(o).get("q")!==t.id&&d(`${s} must be ${i}?q=${t.id} so the pill can pick`);const c=Se(a),l=(await ft(e)).find(f=>f.topic===c);l||d(`${s} names ${i}, which is not a recorded prototype; record it with board.mjs prototype add ${c} --title "\u2026" --take A:"\u2026", then write its take files`);const u=l.takes.map(f=>f.key),w=t.options.map(f=>f.id),p=w.map(f=>u.indexOf(f));if(p.some((f,m)=>f<0||m>0&&f<p[m-1])&&d(`${n}.options ids ${w.join(", ")} must be a non-empty subset of ${i}'s takes, in take order: ${u.join(", ")}`),!l.served)for(const f of u)await P.stat(On(e,c,f)).then(m=>m.isFile(),()=>!1)||d(`${s} take ${f} has no file yet; write ${On(e,c,f)}`)}r(ws,"checkPrototype");async function Lc(e,t,n=async()=>{}){A(e);const a=Array.isArray(t)?t:[t];a.length||d("the question file is empty");const i=await $(I(e),{topics:[],items:[]}),o=i.items||[],s=await $(ct(e),{}),c=new Set([...o,...Object.values(s).flat()].map(p=>p.id)),l=new Set((i.topics||[]).map(p=>p.id));for(const[p,f]of a.entries()){const m=`question ${f?.id??p+1}`;Ue(f,["id","topic","source","question","context","url","urgent","options","round_of","reask_of","ask","suggested","prototype_gate"],m),c.has(ge(f.id,`${m}.id`))&&d(`question id "${f.id}" is already used`),c.add(f.id),j(f.topic,`${m}.topic`),f.topic.startsWith("item:ask:")?await pt(e,f.topic.slice(9),`${m}.topic`):f.topic.startsWith("item:")?await x(e,f.topic.slice(5)):f.topic.startsWith("lineup:")?await Qt(e,f.topic.slice(7)):f.topic.startsWith("wave:")?await Vs(e,f.topic.slice(5)):l.has(f.topic)||d(`topic "${f.topic}" is not declared; add it with: topic add ${f.topic} --title <t>`),j(f.question,`${m}.question`);for(const h of["source","context"])X(f[h],k=>j(k,`${m}.${h}`));X(f.url,h=>ua(h,`${m}.url`)),X(f.urgent,h=>Dt(h,`${m}.urgent`)),ls(f.options,m),X(f.prototype_gate,h=>ms(h,f,m)),X(f.suggested,h=>ds(h,f.options,`${m}.suggested`)),await ws(e,f,m),f.ask!==void 0&&await lr(e,f,m),X(f.round_of,h=>{const k=o.find(v=>v.id===h);k||d(`${m}.round_of names no question "${h}" in owner.json`),k.topic!==f.topic&&d(`${m}.round_of names ${h} on topic ${k.topic}; a round stays on its question's topic, ${f.topic}`),Se(f.url)||d(`${m}.round_of needs a url on a recorded prototype, prototype-<topic>.html?q=${f.id}`)}),X(f.reask_of,h=>{const k=o.find(v=>v.id===h);k||d(`${m}.reask_of names no question "${h}" in owner.json`),k.topic!==f.topic&&d(`${m}.reask_of names ${h} on topic ${k.topic}; a re-ask stays on its question's topic, ${f.topic}`),k.resolved||d(`${m}.reask_of names ${h}, which is not resolved; only a resolved question is asked again`)})}for(const p of new Set(a.map(f=>Se(f.url)).filter(Boolean)))await n(e,p);const u=[],w=[];for(const p of a){const f=p.topic.startsWith("item:")&&!p.topic.startsWith("item:ask:")?p.topic.slice(5):null,m=f&&await Ba(e,f);m&&((s[f]||=[]).push(p),w.push(p),u.push(`staged: ${p.id} waits for ${f}'s ${m} to finish; the card's staged questions then publish together`))}return w.length&&await y(ct(e),s),w.length===a.length?{file:ct(e),lines:u,staged:w.length}:Ca(e,i,o,a.filter(p=>!w.includes(p)),u)}r(Lc,"addQuestions");async function Ba(e,t){const{source:n}=await x(e,t);for(const a of await z(n.dir,R(t).id)){const i=await $(a,null);if(i?.started&&!i.done&&Fe(i)&&i.kind!==ue)return i.kind}return null}r(Ba,"runningPlanner");async function Bt(e,t){if(!e||await Ba(e,t))return;const n=await $(ct(e),{});if(!n[t])return;const{[t]:a,...i}=n,o=await $(I(e),{topics:[],items:[]});await Ca(e,o,o.items||[],a,[]),await y(ct(e),i)}r(Bt,"publishStaged");async function Ca(e,t,n,a,i){const{answers:o,requests:s,reopens:c}=await Ge(e),l=new Date().toISOString(),u=[],w=[];for(const f of a.filter(m=>m.round_of))f.round=sn(n.find(m=>m.id===f.round_of))+1,je(t,[f.round_of],`Followed by prototype round ${f.round}`,l),w.push(f.round_of);for(const f of a.filter(m=>m.reask_of))je(t,[f.reask_of],`Asked again as ${f.id}`,l),w.push(f.reask_of);for(const f of a){const m=Se(f.url),h=n.filter(v=>v.topic===f.topic).map(v=>Ae(v,o,s,[],c)),k=f.topic.startsWith("item:")?za(h):[];for(const v of h)k.includes(v.id)?(je(t,[v.id],`Followed by ${f.id}`,l),w.push(v.id)):!v.resolved&&m&&Se(v.url)===m&&(je(t,[v.id],`Replaced by ${f.id}`,l),u.push(v.id))}const p={...t,topics:t.topics||[],items:[...n,...a.map(f=>({...f,asked_at:l}))]};await y(I(e),p);for(const f of new Set(a.map(m=>qa(m.topic)).filter(Boolean)))await xs(e,f);return{file:I(e),lines:[...i,...u.map(f=>`replaced: ${f}`),...w.map(f=>`resolved: ${f}`)]}}r(Ca,"publishQuestions");function ms(e,t,n){Dt(e,`${n}.prototype_gate`),e&&((!t.topic.startsWith("item:")||t.topic.startsWith("item:ask:"))&&d(`${n}.prototype_gate goes on a card's topic, item:<wave>/<id>`),t.options?.map(a=>a.id).join()!=="yes,no"&&d(`${n}.prototype_gate takes exactly the options yes and no, in that order`))}r(ms,"checkGate");async function Bc(e,t){const n=await $(I(e),{topics:[],items:[]}),a=new Map((n.items||[]).map(c=>[c.id,c])),i=[];for(const c of t){const l=a.get(String(c.id));if(!l?.prototype_gate||!["yes","no"].includes(c.choice)||(je(n,[l.id],`Prototype round: ${c.choice}. The board moved the card on.`),await y(I(e),n),c.choice==="no"))continue;const u=l.topic.slice(5);await Ta(e,u,"moving it to prototyping",async()=>{(await x(e,u)).item.state==="grilling"&&(await we(e,u,{state:"prototyping"}),await U(e,u,{text:"You asked for a prototype round, so the board moved the card to prototyping."}),i.push(u))})}const o=new Map;for(const c of t){const l=a.get(String(c.id)),u=qa(l?.topic);u&&o.set(u,[...o.get(u)||[],hs(l,c)])}const s=[];for(const[c,l]of o)await x(e,c).catch(()=>null)&&await Ta(e,c,"writing them into its notes",async()=>{await Tt(e,c,`Answers (owner, ${Pn()})`,l.join(`
18
+ `)),!(i.includes(c)||!await bs(e,c))&&(await En(e,c,"answers"),s.push(c))});return{prototype:i,staged:s}}r(Bc,"settleAnswers");async function Ta(e,t,n,a){try{await a()}catch(i){if(!(i instanceof ae))throw i;await U(e,t,{text:`Your answers were recorded, but ${n} failed: ${i.message}`})}}r(Ta,"followUp");const qa=r(e=>typeof e=="string"&&e.startsWith("item:")&&!e.startsWith("item:ask:")?e.slice(5):null,"cardOfTopic"),Ct=r(e=>String(e).padStart(2,"0"),"pad2"),Pn=r((e=new Date)=>`${Ct(e.getMonth()+1)}/${Ct(e.getDate())} ${Ct(e.getHours())}:${Ct(e.getMinutes())}`,"noteStamp");function hs(e,t){const n=t.choice===null||t.choice===void 0||t.choice===""?null:(e.options||[]).find(i=>i.id===t.choice)?.label??String(t.choice),a=String(t.note||"").trim();return`- ${e.id} (${e.question}): ${n?`${n}.`:"no option picked."}${a?` Note, verbatim: "${a}"`:""}`}r(hs,"answerLine");const Fa=292,An=r(e=>String(e).toLowerCase().replace(/[^a-z0-9-]+/g,"-"),"notesBase"),gs=r((e,t,n)=>at(g.join(e,`.${An(t)}-notes`),n),"notesLock");function Dn(e,t){let n=`${An(t)}-notes.md`;for(let a=2;de(g.join(e,n));a++)n=`${An(t)}-notes-${a}.md`;return n}r(Dn,"freeNotesName");async function Rn(e,t){const n=g.join(g.dirname(e),`.${g.basename(e)}.${process.pid}.tmp`);await P.rm(n,{force:!0}),await P.writeFile(n,t,{mode:Fa}),await P.rename(n,e)}r(Rn,"writeNotesFile");const $s=r(e=>/\n/.test(j(e,"--heading"))?d("--heading is one line, the section's title"):j(e,"--heading"),"notesHeading"),ks=r((e,t,n)=>`${e.replace(/\n*$/,`
19
+ `)}
20
+ ## ${t}
21
+ ${n.replace(/^\n+|\s+$/g,"")}
22
+ `,"withSection");async function Tt(e,t,n,a){const{source:i,item:{id:o}}=await x(e,t);return gs(i.dir,o,async()=>{const{source:s,wave:c,item:l}=await x(e,t);let u;l.plan?(u=await Wt(s.dir,l.plan,{missingOk:!0}),de(u)||await U(e,t,{text:`The card's notes file ${l.plan} was missing, so the board started a new one.`})):(l.plan=Dn(s.dir,l.id),u=g.join(s.dir,l.plan),await y(s.file,c));const w=de(u)?await P.readFile(u,"utf8"):`# ${l.title}
23
+ `;return await Rn(u,ks(w,$s(n),a)),l.plan})}r(Tt,"appendNotes");async function Ma(e){const{tickets:t}=$e(await ee(e)),n=[],a=new Map;for(const i of await F(e)){const o=await $(i.file,null);a.set(i.slug,{source:i,wave:o||{}});for(const s of o?.items||[])s.plan&&n.push({key:`${i.slug}/${s.id}`,ref:Me(o,s,t),title:s.title,created:s.created||"",archived:!1,dir:i.dir,plan:s.plan})}for(const i of e?await Ve(e):[]){if(!i.item?.plan)continue;const o=a.get(i.wave);n.push({key:i.key,ref:Me(o?.wave||{},i.item,t),title:i.item.title,created:i.item.created||"",archived:!0,dir:o?.source.dir||g.join(e,"data"),plan:i.item.plan})}return n}r(Ma,"cardsWithNotes");const Wa=r(e=>P.realpath(g.resolve(e.dir,e.plan)).catch(()=>null),"realNotes");async function Ua(e,t,n,a){const i=await P.realpath(t).catch(()=>d(`${n} ${t}: no such file`));for(const o of await Ma(e))await Wa(o)===i&&d(`${n} ${t} is the notes file of ${o.ref} "${o.title}" (${o.key}), and a card's notes are its own; ${a(o)}`);return P.readFile(i,"utf8")}r(Ua,"refuseCardNotes");const Nn="write a draft of your own outside the board's data folder";async function Cc(e,t,{heading:n,file:a,from:i}){let o;if(i!==void 0){i===t&&d(`${t} can't fold its own notes into itself; pass --from <the other card>`);const[l,u]=await Promise.all([x(e,t),x(e,i)]),[w,p]=await Promise.all([l,u].map(({source:f,item:m})=>m.plan?P.realpath(g.resolve(f.dir,m.plan)).catch(()=>null):null));w&&w===p&&d(`${i} and ${t} name one notes file, ${l.item.plan}, from before the board named each card's own; the board splits it as it starts`),o=(await Ts(e,i)).markdown,n??=`Folded from ${await J(e,i)} "${u.item.title}"`}else o=await Ua(e,a,"--file",l=>l.key===t?`${Nn}, holding only the new section`:`${Nn}, or fold that card's notes in with item notes ${t} --from ${l.key}`);o.trim()||d(`${i??a} is empty`);const s=await Tt(e,t,n,o),c=await Ft(e,t,"Recorded in the card's notes");return{file:s,heading:n,lines:c}}r(Cc,"addNotes");const Tc=r(e=>Be(e,async()=>{const t=await Ma(e),n=new Map;for(const i of t){const o=await Wa(i);o&&n.set(o,[...n.get(o)||[],i])}const a=[];for(const[i,o]of n){const[s,...c]=o.sort((l,u)=>l.created<u.created?-1:l.created>u.created?1:l.key<u.key?-1:1);for(const l of c.filter(u=>!u.archived)){const{source:u,wave:w,item:p}=await Ne(e,R(l.key).wave).then(m=>({...m,item:m.wave.items.find(h=>h.id===R(l.key).id)})),f=p.plan;p.plan=Dn(u.dir,p.id),await Rn(g.join(u.dir,p.plan),await P.readFile(i,"utf8")),await y(u.file,w),await U(e,l.key,{text:`This card shared its notes file ${g.normalize(f)} with ${s.ref}, so the board gave it its own copy, ${p.plan}.`}),a.push(`${l.key}: its own notes, ${p.plan}, copied from ${g.normalize(f)}, which ${s.key} keeps`)}await P.chmod(i,Fa)}return a}),"splitSharedNotes");async function bs(e,t){const{source:n,item:a}=await x(e,t);return Qe(a)||Re(a)||a.pushed||(await Promise.all((await z(n.dir,a.id)).map(o=>$(o,null)))).some(o=>o?.started&&!o.done&&o.kind!==ue)||(await pe(e,t)).asked.some(o=>o.state==="open")?!1:!await In(e,t)}r(bs,"stageable");const ys={answers:"Staging: your answers are in its notes; the orchestrator judges them next.",implement:"Staging: your Implement is in its notes; the orchestrator judges it next.",reply:"Back to staging: your reply is in its notes; the orchestrator judges it before it goes ready again."};async function En(e,t,n){const{source:a,wave:i,item:o}=await x(e,t);o.state!=="staging"&&(Object.assign(o,{state:"staging",staged_at:new Date().toISOString()}),delete o.unfiled,await y(a.file,i),await U(e,t,{text:ys[n]}))}r(En,"stage");const Ha=r(({text:e,files:t})=>[String(e||"").trim(),...(Array.isArray(t)?t:[]).map(n=>`Attached: ${n?.path||n?.name||n}`)].filter(Boolean).join(`
24
+ `),"messageWords");async function qc(e,t,n){const{item:a}=await x(e,t);a.held&&await si(e,"card-hold",{key:t,hold:!1});const{owner:i,asked:o}=await pe(e,t),s=o.filter(l=>l.state==="open").map(l=>l.id);s.length&&await qt(e,i,s,"Skipped: you tapped Implement");const c=Ha(n);c&&await Tt(e,t,`Implement (owner, ${Pn()}), verbatim`,c),await En(e,t,"implement")}r(qc,"stageImplement");async function Fc(e,t,n){const a=Ha(n);a&&await Tt(e,t,`Reply (owner, ${Pn()}), verbatim`,a),await En(e,t,"reply")}r(Fc,"stageReply");async function Mc(e,t,n){const{source:a,wave:i,item:o}=await x(e,t);!["ready","staging"].includes(o.state)||Qe(o)||(o.state=n==="prototype"?"prototyping":"grilling",delete o.staged_at,await y(a.file,i))}r(Mc,"planAgain");async function xs(e,t){const n=await x(e,t).catch(()=>null);!n||!["ready","staging"].includes(n.item.state)||Qe(n.item)||(n.item.state="grilling",delete n.item.staged_at,await y(n.source.file,n.wave),await U(e,t,{text:"A new question on it, so it is grilling until you answer."}))}r(xs,"grillAgain");function za(e){return e.filter(t=>t.state==="answered").map(t=>t.id)}r(za,"answeredOnClaude");function je(e,t,n,a=new Date().toISOString()){for(const i of e.items||[])t.includes(i.id)&&Object.assign(i,{resolved:n,resolved_at:a})}r(je,"setResolved");async function qt(e,t,n,a){return je(t,n,a),await y(I(e),t),I(e)}r(qt,"markResolved");async function Wc(e,t,n){A(e);const a=await $(I(e),{topics:[],items:[]}),i=(a.items||[]).find(p=>p.id===t);i||d(`no question "${t}" in owner.json`);const{answers:o,requests:s,reopens:c}=await Ge(e),l=Ae(i,o,s,[],c);if(l.state==="resolved")return I(e);l.state==="requested"&&d(`${t} waits on the prototype the owner asked for; question add with round_of ${t} resolves it`);const u=j(n,"--note");Se(i.url)&&vs(a,i,o);const w=await qt(e,a,[t],u);return await Za(e,i.topic),w}r(Wc,"resolveQuestion");async function Uc(e,t,n,a=[]){A(e);const[i,...o]=[...new Set(t)];i||d("question move takes the ids of the questions to merge"),await x(e,n);const s=[n,...a];new Set(s).size!==s.length&&d("question move names a card twice");for(const f of a)await x(e,f);const c=await $(I(e),{topics:[],items:[]}),{answers:l,requests:u,reopens:w}=await Ge(e);for(const f of[i,...o]){const m=(c.items||[]).find(k=>k.id===f)||d(`no question "${f}" in owner.json`),{state:h}=Ae(m,l,u,[],w);h!=="open"&&d(`${f} is ${h}; question move takes open questions`)}for(const[f,m]of a.entries()){const{item:h}=await x(e,m),k=[...h.deps||[],s[f]].join(",");await we(e,m,{deps:k,relate:n})}const p=await J(e,n);return Object.assign(c.items.find(f=>f.id===i),{topic:`item:${n}`}),je(c,o,`Duplicate of ${p}'s question ${i}`),await y(I(e),c),I(e)}r(Uc,"moveQuestions");async function Hc(e,t){const n=t.filter(({choice:i,note:o})=>!i&&String(o||"").trim()).map(i=>i.id),a=n.length?await $(I(e),{topics:[],items:[]}):null;a?.items?.some(i=>n.includes(i.id))&&(je(a,n,"Answered with a note"),await y(I(e),a))}r(Hc,"resolveNoteAnswers");function vs(e,t,n){const a=sn(t),i=e.items.filter(o=>o.topic===t.topic&&sn(o)===a).map(o=>Ae(o,n));Gi(i,a)||d(`${t.id} is in prototype round ${a}, which no pick has settled yet; a pick settles it`)}r(vs,"roundSettled");async function Ge(e){const t=await K(g.join(e,"data","events.jsonl"));return{answers:await Promise.all(t.filter(a=>a.kind==="owner-answer").map(async a=>{const i=await Promise.all(["html","json"].map(o=>P.stat(g.join(e,"data","replies",`${a.id}.${o}`)).then(s=>s.isFile(),()=>!1)));return{...a,reply:a.reply||i.some(Boolean)}})),requests:t.filter(a=>a.kind==="prototype-request"),reopens:t.filter(a=>a.kind===zi)}}r(Ge,"ownerEvents");async function zc(e,t){const{asked:n}=await _s(e),a=new Map(n.map(i=>[i.id,i.state]));return t.filter(i=>!a.has(String(i.id))||["open","requested"].includes(a.get(String(i.id))))}r(zc,"unrecorded");async function _s(e){const t=await $(I(e),{topics:[],items:[]}),{answers:n,requests:a,reopens:i}=await Ge(e);return{owner:t,asked:(t.items||[]).map(o=>Ae(o,n,a,[],i))}}r(_s,"topicQuestionsAll");async function Ka(e,t){const n=await $(I(e),{topics:[],items:[]}),{answers:a,requests:i,reopens:o}=await Ge(e),s=(n.items||[]).filter(c=>c.topic===t).map(c=>Ae(c,a,i,[],o));return{owner:n,asked:s}}r(Ka,"topicQuestions");const pe=r((e,t)=>Ka(e,`item:${t}`),"cardQuestions"),Ft=r((e,t,n)=>Ja(e,`item:${t}`,n),"resolveAnswered");async function Ja(e,t,n){if(!e)return[];const{owner:a,asked:i}=await Ka(e,t),o=za(i);return o.length&&await qt(e,a,o,n),await Za(e,t),o.map(s=>`resolved: ${s}`)}r(Ja,"resolveAnsweredOn");const Ga=r(async(e,t)=>!!e&&(await pe(e,t)).asked.some(n=>n.state==="open"),"hasOpenQuestion");function Qa(e,t,n){e.next=j(t,"--next"),n?e.next_on_questions=!0:delete e.next_on_questions}r(Qa,"setNext");async function Za(e,t){const n=t.startsWith("item:")&&!t.startsWith("item:ask:")?t.slice(5):null;if(!n)return;let a;try{a=await x(e,n)}catch(c){if(c instanceof ae)return;throw c}const{source:i,wave:o,item:s}=a;s.next_on_questions&&((await pe(e,n)).asked.some(c=>c.state!=="resolved")||(delete s.next,delete s.next_on_questions,await y(i.file,o)))}r(Za,"clearWaitedNext");async function Ss(e,t){const{source:n,wave:a,item:i}=await x(e,t);i.next===void 0&&i.next_on_questions===void 0||(delete i.next,delete i.next_on_questions,await y(n.file,a))}r(Ss,"clearNext");async function Va(e,t,n,a){if(!e)return async()=>[];const{owner:i,asked:o}=await pe(e,t),s=o.filter(l=>l.state==="open").map(l=>l.id);s.length&&!a&&d(`${n}: ${t} has ${s.length===1?"an open question":"open questions"} ${s.join(", ")} with no answer yet; pass --force to close the card and resolve ${s.length===1?"it":"them"} too`);const c=o.filter(l=>l.state!=="resolved").map(l=>l.id);return async()=>(c.length&&await qt(e,i,c,"Closed with the card"),c.map(l=>`resolved: ${l}`))}r(Va,"closeQuestions");async function Kc(e,t,n,a,i){await x(e,t),j(n,"--text"),X(a,o=>j(o,"--owner-step")),X(i,o=>ri(o)),await Va(A(e),t,"land",!1)}r(Kc,"checkClose");async function In(e,t){const n=await $(I(e),{items:[]}),{answers:a,requests:i,reopens:o}=await Ge(e);return Hi((n.items||[]).map(s=>Ae(s,a,i,[],o)),t)}r(In,"unpickedPrototype");const js=r(e=>e.length>120?`${e.slice(0,119)}\u2026`:e,"clip"),Os={ask:"text","ask-edited":"text","item-message":"text","owner-answer":"note"};async function Ya(e,t,n){const a=j(t,n).replace(/\s+/g," ");return(await K(g.join(e,"data","events.jsonl"))).map(o=>String(o.data?.[Os[o.kind]]??"").replace(/\s+/g," ").toLowerCase()).some(o=>o.includes(a.toLowerCase()))||d(`${n} must quote the owner's words in an ask, card message or answer note on this board`),a}r(Ya,"ownerQuote");async function Jc(e,t,n){return A(e),n===void 0&&d(`only the owner's Serve / Build and the hub's Auto-build start a card run; pass --asked "<the owner's words>" quoting where the owner asked for ${t}'s run`),`Running at the owner's ask: "${await Ya(e,n,"--asked")}"`}r(Jc,"askedGate");async function pt(e,t,n="--from-ask"){return ge(t,n),(await K(g.join(A(e),"data","events.jsonl"))).some(i=>i.id===t&&i.kind==="ask")?t:d(`no ask "${t}" in this site's data/events.jsonl`)}r(pt,"checkAsk");function Ln(e,t,n=new Date().toISOString()){if(!jt[t])throw new Error(`no lane "${t}"`);const a=e.lane;return e.lane=t,e.lane_at=n,t==="done"?(e.closed_at=n,delete e.reopened_at):a==="done"&&(e.reopened_at=n),a}r(Ln,"placeIn");async function Gc(e,t,n,a){const{source:i,wave:o,item:s}=await x(e,t);if(s.lane===n)return null;const c=new Date().toISOString(),l=Ln(s,n,c);if(await y(i.file,o),l===void 0)return null;const u={ts:c,who:"claude",kind:na,text:`Moved to ${jt[n]}: ${j(a,"why")}`};return await Y(W(e,t),u),u.text}r(Gc,"moveCard");const G=r(e=>e?.lane==="done"||!!e?.reopened_at,"closed"),Qe=r(e=>!!e?.lane&&e.lane!=="todo","leftTodo"),Qc=r(async(e,t)=>G((await x(e,t)).item),"isFinished"),Ps=["ready","grilling","prototyping"];function Bn(e){e==="staging"&&d("--state staging is set by the board, on an owner action; pass ready, grilling or prototyping"),e!==void 0&&!Ps.includes(e)&&d("--state takes ready, grilling or prototyping")}r(Bn,"checkState");const As="a card goes ready only from staging, which the board sets on the owner's Send, Implement or Reply";async function Ds(e,t){return(await K(g.join(A(e),"data","events.jsonl"))).some(a=>a.kind==="ask"&&a.id===t&&a.data?.quick===!0||a.kind==="item-message"&&a.data?.key===`ask:${t}`&&a.data?.intent==="build")}r(Ds,"quickAsk");const Rs=/^mark (?:it |this |the card )?ready\b/i;function Ns(e,t,n){const a=[].concat(t).map(s=>pn(s)||d(`--pr takes a PR number or owner/repo#<n>, not "${s}"`)),i=r((s,c)=>s.number===c.number&&(s.repo||n||null)===(c.repo||n||null),"same"),o=st(e);for(const s of a)o.some(c=>i(c,s))||o.push(s);delete e.pr,e.prs=o}r(Ns,"addPRs");const H=r((e,t)=>String(t).includes("/")?String(t):`${e}/${t}`,"depKey");async function Ze(e,t,n){const a=(n.deps||[]).map(o=>H(t,o)),i=await Promise.all(a.map(o=>x(e,o).then(({item:s})=>s,()=>null)));return a.filter((o,s)=>i[s]&&!G(i[s]))}r(Ze,"unfinishedDeps");async function Es(e,t,n){const a=await Promise.all((await Ze(e,R(t).wave,n)).map(i=>J(e,i)));return n.blocked_until?[...a,n.blocked_until]:a}r(Es,"blockerNames");async function Mt(e,t,n){n??=(await x(e,t)).item;const a=await Promise.all((await Ze(e,R(t).wave,n)).map(i=>J(e,i)));return[a.length&&`by ${a.join(", ")}`,n.blocked_until&&`until ${n.blocked_until}`].filter(Boolean).join(" and ")}r(Mt,"blockWords");const Is=r((e,t)=>!Re(e)&&!e.pushed&&!e.merge_ready&&!G(e)&&(!!nn(t)||t.some(n=>n?.started&&!n.done&&tt(n))),"inProgress");async function Ls(e,t){const n=[];for(const a of await F(e)){const i=await $(a.file,null);for(const o of Array.isArray(i?.items)?i.items:[])Qe(o)||o.blocked_until||!(o.deps||[]).some(s=>H(a.slug,s)===t)||(await Ze(e,a.slug,o)).length||n.push(`${a.slug}/${o.id}`)}return n}r(Ls,"unblockedBy");async function Xa(e,t,n,a){const i=new Set(a.match(/[\w-]+(?:\.[\w-]+)*(?:\/[\w-]+(?:\.[\w-]+)*)*/g)||[]),o=new Set((n.deps||[]).map(s=>H(R(t).wave,s)));for(const s of await F(e)){const c=await $(s.file,null);for(const l of c?.items||[]){const u=`${s.slug}/${l.id}`;if(u===t||o.has(u)||G(l))continue;[l.id,Me(c,l)].flatMap(p=>[p,`${s.slug}/${p}`]).some(p=>i.has(p))&&d(`--next names unfinished card ${u}; add it with --deps`)}}}r(Xa,"checkNextDeps");const ei=r(e=>`item notes ${e} --heading "<heading>" --file <draft.md>`,"NOTES_COMMAND");function Bs(e,t){t.size||d(`${e} has no size; a builder starts from it, so pass --size small or large with --state ready`),t.plan||d(`${e} has no notes file; a builder starts from it, so add its notes with ${ei(e)} before --state ready`)}r(Bs,"checkSizeAndNotes");async function Cs(e,t,n,a){Bs(t,a);const i=await P.readFile(await Wt(n,a.plan),"utf8"),o=(await pe(e,t)).asked.filter(s=>s.answers.length&&!i.includes(s.id)&&!(s.prototype_gate&&!s.answers.at(-1).note)).map(s=>s.id);o.length&&d(`${t}'s notes leave out the answered question${o.length===1?"":"s"} ${o.join(", ")}; record each answer in its notes, naming its id, with ${ei(t)} before it is ready`)}r(Cs,"checkNotesComplete");async function Wt(e,t,{missingOk:n=!1}={}){const a=r(s=>d(`a notes file is a .md file inside ${e}, named relative to it: "${t}" ${s}`),"bad");(typeof t!="string"||!t.trim())&&a("is empty"),(g.isAbsolute(t)||t.split(/[\\/]/).includes(".."))&&a("leaves the folder"),g.extname(t).toLowerCase()!==".md"&&a("is not a .md file");const i=await P.realpath(e),o=await P.realpath(g.resolve(e,t)).catch(()=>n?g.resolve(i,t):a("does not exist"));return o.startsWith(i+g.sep)||a("leaves the folder"),o}r(Wt,"planPath");async function Ts(e,t){const{source:n,wave:a,item:i}=await x(e,t);if(i.plan)return{wave:a,item:i,markdown:await P.readFile(await Wt(n.dir,i.plan),"utf8")};if(typeof i.notes=="string"&&i.notes.trim())return{wave:a,item:i,markdown:i.notes};d(`${t} has no notes`)}r(Ts,"readNotes");function ti({install:e,installSha:t,installLabel:n,clearInstall:a}){if(e===void 0)return(t!==void 0||n!==void 0)&&d("--install-sha and --install-label go with --install <url>"),a?null:void 0;a&&d("pass --install or --clear-install, not both");let i;try{i=new URL(String(e).trim())}catch{i=null}(!i||!["https:","itms-services:"].includes(i.protocol))&&d(`--install takes an https:// or itms-services:// URL, not "${e}"`),i.protocol==="itms-services:"&&!(i.searchParams.get("action")==="download-manifest"&&/^https:\/\//.test(i.searchParams.get("url")||""))&&d("--install: an itms-services:// URL needs ?action=download-manifest&url=https://\u2026/manifest.plist");const o=t===void 0?void 0:/^[0-9a-f]{7,40}$/i.test(t)?t.toLowerCase():d(`--install-sha takes a commit sha of 7 to 40 hex digits, not "${t}"`);return{url:String(e).trim(),...n===void 0?{}:{label:j(n,"--install-label")},built_at:new Date().toISOString(),...o?{sha:o}:{}}}r(ti,"installOf");async function Zc(e,{install:t,installSha:n}){const a=ti({install:t,installSha:n});a||d("main set needs --install <url>");const i=g.join(A(e),"data","main.json");return await y(i,{install:a}),{file:i,lines:["main install set"]}}r(Zc,"setMainInstall");const ni=r((e,t)=>e.length?`Merged PR${e.length>1?"s":""} ${We(e,t)}.`:"Merged.","mergedLine");function qs(e,t,n,a,i){return n?a??ni(n,i):(a===void 0&&e&&d(`--done needs --text "<what was done, or why it closes without landing>", which becomes ${t}'s last thread line`),a??null)}r(qs,"closingLine");function Fs(e,t,n){const a=new Date().toISOString(),i=r((o,s)=>o.number===s.number&&(o.repo||n||null)===(s.repo||n||null),"same");e.prs=st(e).map(o=>{const s=t.find(c=>i(o,c));return s&&!o.merged_at?{...o,merged_at:s.at||a}:o}),delete e.pr}r(Fs,"markMerged");async function ai(e,t,{ticket:n,status:a,clearStatus:i,done:o,merged:s,start:c,handBack:l,next:u,clearNext:w,pr:p,pushed:f,mergeReady:m,ghSince:h,fromAsk:k,state:v,held:E,unheld:O,pickUpAt:B,deps:S,blockedUntil:_,blockReason:N,parent:T,related:C,relate:M,unrelate:L,install:ke,installSha:me,installLabel:ie,clearInstall:be,title:re,size:Ye,model:gt,force:ce,text:oe,who:Zt,by:$t,command:kt,ownerStep:ye,ownerDone:Q,check:Oe,stopRun:le},Hn){(a!==void 0||i)&&d(`--status${i?" and --clear-status are":" is"} gone: a card's lane is stored and moves itself. Close it with --done --text "<what was done, or why it closes without landing>"`),s&&(o=!0),C!==void 0&&(M!==void 0||L!==void 0)&&d("pass --related, or --relate and --unrelate, not both"),(v!=="staging"||k===void 0)&&Bn(v),ce&&!o&&d("--force goes with --done"),k!==void 0&&o&&d("--from-ask opens a card; close it in another call"),oe!==void 0&&j(oe,"--text"),ye!==void 0&&j(ye,"--owner-step");const Xe=Oe===void 0||Oe===""?Oe:ri(Oe);oe!==void 0&&k===void 0&&!o&&d("--text goes with --from-ask or --done"),k!==void 0&&oe===void 0&&d(`--from-ask needs --text "<first line>", the item's first thread line after the ask`),u!==void 0&&w&&d("pass --next or --clear-next, not both"),typeof u=="string"&&!u.trim()&&([u,w]=[void 0,!0]);const Vt=Ms({held:E,unheld:O,pickUpAt:B}),bt=ti({install:ke,installSha:me,installLabel:ie,clearInstall:be});[o,c,l,u,w,p,f,m,h,k,v,Vt,S,_,N,T,C,M,L,bt,re,Ye,gt,n,ye,Oe].every(D=>D===void 0||D===!1)&&d("nothing to set: pass --done, --next, --clear-next, --pr, --pushed, --gh-since, --from-ask, --state, --held, --unheld, --pick-up-at, --deps, --blocked-until, --block-reason, --parent, --related, --relate, --unrelate, --install, --clear-install, --title, --size, --model, --ticket or --check");const Z=k===void 0?void 0:await pt(e,k);Z&&await $n(e,Z);const zn=Z!==void 0&&await Ds(e,Z);v==="staging"&&!zn&&Bn(v);const{source:xe,wave:Te,item:b}=await x(e,t);v==="ready"&&zn&&!["staging","ready"].includes(b.state)&&d(`${t} came from a Quick build ask: file it --state staging, and the orchestrator judges it before it goes ready`),v==="ready"&&!["staging","ready"].includes(b.state)&&d(`${t} reads ${b.state||"no state"}, not staging; ${As}`),Z&&b.from_ask&&b.from_ask!==Z&&d(`${t} came from ask "${b.from_ask}", so ask "${Z}" can't be its from-ask; to fold it in: item relate-ask ${Z} ${t} --summary "<line>"`),Z&&v===void 0&&!b.state&&d("--from-ask files a card, which reads New until it has a state: pass --state grilling or prototyping, or staging for a Quick build ask");const Kn=G(b);ye!==void 0&&Kn&&d(`${t} is already Done, so its ticket closed; an owner step goes on the command that finishes the card: land --owner-step, or builder done before the merge`),ye!==void 0&&(b.owner_step=j(ye,"--owner-step")),Q&&delete b.owner_step,Xe!==void 0&&!b.owner_step&&d(`--check goes with an owner step, which ${t} doesn't have: pass --owner-step "<the step>" with it on builder done or land`),Xe&&(b.owner_check={check:Xe,since:b.owner_check?.check===Xe?b.owner_check.since:new Date().toISOString()}),(Xe===""||!b.owner_step)&&delete b.owner_check;const yt=await Promise.all((await z(xe.dir,b.id)).map(D=>$(D,null))),Ni=on(b,yt);if(re!==void 0&&(b.title=j(re,"--title")),ii(b,{size:Ye,model:gt}),S!==void 0){const D=await ci(e,R(t).wave,S);D.length?b.deps=D:delete b.deps}const Jn=_===""?b.blocked_until:void 0;if(_!==void 0&&(_===""?delete b.blocked_until:b.blocked_until=j(_,"--blocked-until")),N&&(!b.deps?.length&&!b.blocked_until&&d("--block-reason goes with a block: --deps or --blocked-until"),b.block_reason=j(N,"--block-reason")),(N===""||!b.deps?.length&&!b.blocked_until)&&delete b.block_reason,T!==void 0){const D=await li(e,R(t).wave,t,T);D?b.parent=D:delete b.parent}const Gn=T!==void 0||C!==void 0||M!==void 0||L!==void 0?await Cn(e,t):null;let Yt=C===void 0?null:await Us(e,t,b,C);M!==void 0&&(b.related=[...new Set([...(b.related||[]).map(D=>H(R(t).wave,D)),...await Ht(e,t,M,"--relate")])]),L!==void 0&&(Yt=await Hs(e,t,b,await Ht(e,t,L,"--unrelate"))),Vt!==void 0&&Ut(b,Vt),c&&b.held&&d(b.pick_up_at?`${t} is held until ${b.pick_up_at}, when the board resumes it; start it early only with item set ${t} --unheld`:`${t} is held; start it only once the owner resumes it, with item set ${t} --unheld`);const et=p!==void 0||f!==void 0||m!==void 0;if((c||o||l||et)&&Ws(b),c){const D=await Ze(e,R(t).wave,b);D.length&&d(`${t} is blocked by ${D.join(", ")}; start it once ${D.length===1?"that is":"those are"} done`),b.blocked_until&&d(`${t} is blocked until ${b.blocked_until}; start it once that holds and the block is cleared with --blocked-until ""`)}const xt=c&&e?await In(e,t):null;xt&&d(`${t} has no take picked: the owner's answer to ${xt.id} picks none${xt.note?` ("${js(xt.note)}")`:""}, so it prototypes again: draw the next round from that note, or grill when the note asks a question, and build once a take is picked`);const Ei=o?await Va(e,t,"--done",ce):async()=>[],Qn=o?qs(e,t,s,oe,te(e,Te)):null;if(s&&Fs(b,s,te(e,Te)),u!==void 0&&(Qa(b,u,await Ga(e,t)),await Xa(e,t,b,b.next)),w&&(delete b.next,delete b.next_on_questions),c&&u===void 0&&b.next_on_questions&&(delete b.next,delete b.next_on_questions),et&&!b.review_since&&(b.review_since=new Date().toISOString()),p!==void 0&&Ns(b,p,te(e,Te)),f!==void 0){fe().find(D=>he(D.dir,A(e)))?.remote||d("--pushed is for a board whose repo is a plain git remote; on GitHub, open a PR and pass --pr"),Re(b)&&d(`${t} has a PR; its PR says when it merges`);try{po(f)}catch{d("--pushed takes the name of the branch you pushed")}b.pushed={branch:f,at:new Date().toISOString()},b.branch=f}if(m!==void 0&&(b.merge_ready={branch:m,at:new Date().toISOString()},b.branch=m),o&&delete b.merge_ready,h!==void 0&&((typeof h!="string"||!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(h)||Number.isNaN(Date.parse(h)))&&d("--gh-since takes an ISO-8601 UTC time such as 2026-10-23T11:00:00Z"),b.gh_since=h),Z&&(b.from_ask=Z),v==="ready"&&e){const D=(await pe(e,t)).asked.filter(qe=>qe.state==="open").map(qe=>qe.id);D.length&&d(`${t} has ${D.length===1?"an open question":"open questions"} ${D.join(", ")}; it is ready once the owner has answered ${D.length===1?"it":"them"}`),await Cs(e,t,xe.dir,b)}v!==void 0&&(v!=="ready"&&["staging","ready"].includes(b.state)&&(b.round_at=new Date().toISOString()),b.state=v,v==="staging"?b.staged_at=new Date().toISOString():delete b.staged_at,delete b.unfiled),b.state==="ready"&&Rs.test(b.next??"")&&d(`${t} would read ready with Next "${b.next}", which asks for the state it is already in; pass --next with what happens next, or --clear-next`),bt&&(b.install=bt),bt===null&&delete b.install,!Ni&&!c&&on(b,yt)&&d(`that leaves ${t} In Progress with nothing building and no Next; pass --next "<what happens next>"`);const vt=(S!==void 0||_)&&!o&&Is(b,yt)?await Mt(e,t,b):"",Ii=vt&&yt.some(D=>D?.started&&!D.done&&tt(D))?await le():[],Zn=te(e,Te);let Xt=null;const _t=r((D,qe)=>{b.lane!==D&&Ln(b,D)!==void 0&&qe&&(Xt=`Moved to ${jt[D]}: ${qe}`)},"moveTo");o?_t("done",null):vt?_t("todo",null):et&&b.lane!=="done"?_t("review",p!==void 0?`PR ${We([].concat(p).map(pn),Zn)} opened`:f!==void 0?`${f} pushed for its merge`:"verified, it waits for your Merge"):c&&!G(b)&&_t("progress","a build started"),(n!==void 0||Z&&!b.ticket&&!b.linear?.length)&&await Tn(e,t,Te,b,n,Hn,xe.dir),await y(xe.file,Te),Xt&&await Y(W(e,t),{ts:b.lane_at,who:"claude",kind:na,text:Xt}),vt&&(await Un(xe.dir,b.id,!1,{endHandBack:!0}),await Y(W(e,t),{ts:new Date().toISOString(),who:"claude",text:`Blocked ${vt}: back to Todo. It starts again once nothing blocks it.`})),Yt&&await Yt(),Gn&&await ui(e,t,Gn,await Cn(e,t));const ve=[...Ii,...await Ei()];v==="ready"&&e&&ve.push(...await Ft(e,t,"Recorded in the card's notes")),c&&ve.push(...await Ft(e,t,"Acted on: build started")),Z&&ve.push(...(await U(e,t,{text:oe})).lines),Qn&&e&&ve.push(...(await U(e,t,{text:Qn,who:Zt,by:$t,command:kt})).lines);const Vn=b.owner_step&&o?null:oo({start:c,done:o,handover:et}),Yn=Vn&&await so(await fi(e),b,Vn,{...Hn,repo:Zn});Yn&&ve.push(Yn),(et||o||l)&&(await Un(xe.dir,b.id,o,l?{handedBack:new Date().toISOString()}:{endHandBack:!0}),await Bt(e,t));const en=Z?await Ie(e):{};if(V(en,Z)!==void 0&&(delete en[Z],await y(_e(e),{links:en})),Jn&&!G(b)&&!(await Ze(e,R(t).wave,b)).length&&(await Y(W(e,t),{ts:new Date().toISOString(),who:"claude",text:`Unblocked: ${Jn}.`}),await se({dir:e},"card-unblocked",{key:t}),ve.unshift(`unblocked: ${t}`)),!o||Kn)return{file:xe.file,lines:ve};const Xn=await Ls(e,t),Li=new Date().toISOString();for(const D of Xn)await Y(W(e,D),{ts:Li,who:"claude",text:`Unblocked: ${await J(e,t)} is done.`}),await se({dir:e},"card-unblocked",{key:D});return{file:xe.file,lines:[...Xn.map(D=>`unblocked: ${D}`),...ve]}}r(ai,"changeItem");function ii(e,{size:t,model:n}){for(const[a,i,o]of[["size",t,["small","large"]],["model",n,["opus","sonnet"]]])i!==void 0&&(i===""?delete e[a]:e[a]=o.includes(i)?i:d(`--${a} takes ${o.join(" or ")}, or "" to clear it`))}r(ii,"setBuildChoice");function Ms({held:e,unheld:t,pickUpAt:n}){return[e,t,n!==void 0].filter(Boolean).length>1&&d("pass one of --held, --unheld and --pick-up-at"),n!==void 0?n===""?null:{until:oi(n)}:e?{until:null}:t?null:void 0}r(Ms,"holdOf");function oi(e){const t=typeof e=="string"&&ao.test(e)?Date.parse(e):NaN;return Number.isNaN(t)&&d('--pick-up-at takes an ISO time such as 2026-11-04T09:00, the host\u2019s local time unless it names an offset, or "" to resume the card'),t<=Date.now()&&d(`--pick-up-at ${e} has passed; pass a later time, or --unheld to resume the card now`),new Date(t).toISOString()}r(oi,"pickUpTime");function Ws(e){delete e.held,delete e.pick_up_at}r(Ws,"clearHold");function Ut(e,t){t?e.held=!0:delete e.held,t?.until?e.pick_up_at=t.until:delete e.pick_up_at,e.held_at=new Date().toISOString()}r(Ut,"setHold");async function Vc(e,t,n,{announce:a=!0}={}){const{source:i,wave:o,item:s}=await x(e,t);if(a){const c=new Date(n).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});await Y(W(e,t),{ts:new Date().toISOString(),who:"claude",text:`Resumed: its hold until ${c} ended.`}),!s.blocked_until&&!(await Ze(e,R(t).wave,s)).length&&await se({dir:e},"card-unblocked",{key:t,until:n})}Ut(s,null),await y(i.file,o)}r(Vc,"pickUp");async function si(e,t,n){const a=String(n?.key||""),{wave:i,id:o}=R(a),{source:s,wave:c}=await Ne(e,i),l=n.shared_by?n.shared_by.name||n.shared_by.login:void 0,u={text:Pe.done,who:"love",by:l,command:"done"},w=c.items.find(h=>h.id===o)||d(`wave "${i}" has no item "${o}"`);if(!(t==="card-done"&&!!w.owner_step)&&w.lane==="done")return null;if(t==="card-done")return we(e,a,{done:!0,force:!0,ownerDone:!0,...u});const f=n.hold?typeof n.until=="string"?new Date(n.until).toISOString():null:void 0;Ut(w,n.hold?{until:f}:null),await y(s.file,c),n.hold&&(await Un(s.dir,o,!1,{held:!0}),await Bt(e,a));const m=f&&new Date(f).toLocaleString("en-US",{weekday:"short",month:"numeric",day:"numeric",hour:"numeric",minute:"2-digit"});return U(e,a,n.hold?{text:m?`${Pe.hold} until ${m}`:Pe.hold,who:"love",by:l,command:"hold"}:{text:Pe.resume,who:"love",by:l,command:"resume"})}r(si,"recordOwnerTap");function ri(e){try{return Po(Oo(e))}catch(t){return d(t.message)}}r(ri,"checkSpec");async function Yc(e,t,n,a){const{item:i}=await x(e,t);return!i.owner_step||i.owner_check?.check!==n?null:we(e,t,{done:!0,force:!0,ownerDone:!0,text:`Check passed: ${a}. Closed.`})}r(Yc,"passOwnerCheck");async function Xc(e,t,n,a){const{source:i,wave:o,item:s}=await x(e,t);return!s.owner_step||s.owner_check?.check!==n?!1:(s.owner_check.broken=a,await y(i.file,o),await U(e,t,{text:`The check for "${s.owner_step}" can't run (${a}), so it stopped; Claude is asked to fix it. Done still closes the card.`}),await se({dir:e},"owner-check-broken",{key:t,check:n,error:a}),!0)}r(Xc,"breakOwnerCheck");async function ci(e,t,n){const a=[...new Set(String(n).split(",").map(i=>i.trim()).filter(Boolean))];for(const i of a)await x(e,H(t,i));return a}r(ci,"depList");async function li(e,t,n,a){if(!String(a).trim())return null;const i=H(t,String(a).trim());i===n&&d("--parent: a card can't be its own Parent");for(let o=i;o;){const s=o===i?await x(e,o):await x(e,o).catch(l=>{if(l instanceof ae)return null;throw l});if(!s)break;const{item:c}=s;c.parent&&H(R(o).wave,c.parent)===n&&d(`--parent: ${i} is a child of ${n}, so it can't be its Parent`),o=c.parent?H(R(o).wave,c.parent):null}return i}r(li,"parentKey");async function Ht(e,t,n,a){const i=t.split("/")[0],o=[...new Set(String(n).split(",").map(s=>s.trim()).filter(Boolean).map(s=>H(i,s)))];o.includes(t)&&d(`${a}: a card can't be related to itself`);for(const s of o)await x(e,s);return o}r(Ht,"relatedKeys");const di=r((e,t,n)=>async()=>{for(const a of await F(e)){const i=await $(a.file,null);let o=!1;for(const s of Array.isArray(i?.items)?i.items:[]){const c=`${a.slug}/${s.id}`;c===t||!n(c)||!(s.related||[]).some(l=>H(a.slug,l)===t)||(s.related=s.related.filter(l=>H(a.slug,l)!==t),s.related.length||delete s.related,o=!0)}o&&await y(a.file,i)}},"dropLinksTo");async function Cn(e,t){const n=new Set;let a=null;for(const i of await F(e))for(const o of(await $(i.file,null))?.items||[]){const s=`${i.slug}/${o.id}`,c=(o.related||[]).map(l=>H(i.slug,l));s===t?(c.forEach(l=>n.add(l)),o.parent&&(a=H(i.slug,o.parent))):c.includes(t)&&n.add(s)}return{related:n,parent:a}}r(Cn,"linksOf");async function ui(e,t,n,a){const i=r(async(o,s,c=o,l=o)=>{await U(e,t,{text:Pe[c],command:c,card:s}),await U(e,s,{text:Pe[l],command:l,card:t})},"both");for(const o of a.related)n.related.has(o)||await i("related",o);for(const o of n.related)a.related.has(o)||await i("unrelated",o);a.parent&&a.parent!==n.parent&&await i("followup-of",a.parent,"followup-of","followup")}r(ui,"postLinkLines");async function Us(e,t,n,a){const i=await Ht(e,t,a,"--related");return i.length?n.related=i:delete n.related,di(e,t,o=>!i.includes(o))}r(Us,"setRelated");function Hs(e,t,n,a){const i=(n.related||[]).filter(o=>!a.includes(H(R(t).wave,o)));return i.length?n.related=i:delete n.related,di(e,t,o=>a.includes(o))}r(Hs,"unrelateCards");const te=r((e,t)=>e&&fe().find(n=>he(n.dir,e))?.github||t.repo,"boardRepoOf");async function el(e,t){const{wave:n,item:a}=await x(e,t);Re(a)||d(`${t} has no PR`);const i=De(a,te(e,n));i.length||d(`${t} has no repo`);const o=a.gh_since||"1970-01-01T00:00:00Z";try{const s=$e(await ee(e)).github_login||await $o();return await Promise.all(i.map(async({repo:c,number:l})=>({repo:c,...sa(await oa({repo:c,pr:l,since:o}),{pr:l,since:o,me:s})})))}catch(s){d(s.message)}}r(el,"prReport");async function tl(e,t,{replyTo:n,body:a}){const i=n===void 0?"pr comment":"pr reply",o=pn(t)||d(`${i} takes a PR number or owner/repo#<n>`);n!==void 0&&!/^[1-9]\d*$/.test(n)&&d("pr reply takes the id of the review comment it answers");const s=j(a,"--body").trim();(s.startsWith(it.trim())||s.includes(fn))&&d(`${i} adds the ${it.trim()} prefix and the claude-bridge marker itself; pass only your words`);const c=o.repo||te(A(e),{})||d(`${i} needs a board on GitHub; this board has no repo there`);try{const l=await ko({repo:c,pr:o.number,replyTo:n,words:s,login:$e(await ee(e)).github_login}),u=await ja(e,ot({repo:c,number:o.number}),"comment",`Comment on #${o.number}: ${s}`);return{lines:[`posted on PR ${o.repo?t:`#${t}`}: ${l}`,...u.length?[`queued for ${u.join(", ")}'s Slack thread`]:[]]}}catch(l){d(l.message)}}r(tl,"postPR");const fi=r(async e=>Na((await ee(e)).tickets),"boardSource");async function nl(e,t,{afterApproval:n,confirmed:a,tapped:i}={}){A(e);const{wave:o,item:s}=await x(e,t);Re(s)||d(`${t} has no PR`),G(s)&&d(`${t} is done`);const c=await Mt(e,t,s);c&&(i&&await U(e,t,{text:`Not merged: blocked ${c}.`}),d(`Not merged: blocked ${c}.`));const l=te(e,o),u=De(s,l);u.length||d(`${t} has no repo`);const w=new Date().toISOString();let p;try{p=await Promise.all(u.map(_=>oa({repo:_.repo,pr:_.number,since:w})))}catch(_){d(_.message)}const f=p.map(({view:_})=>_);if(xo(f)){const _=u.map((N,T)=>({...N,at:f[T].mergedAt})).filter((N,T)=>f[T].state==="MERGED");return we(e,t,{merged:_,text:`PR${_.length>1?"s":""} ${We(_,l)} ${_.length>1?"were":"was"} already merged.`})}const m=u.map((_,N)=>({..._,...p[N]})).filter(({view:_})=>_.state==="OPEN"),h=mn(o,(await ee(e)).reviewers||[]).length>0,k=[];let v=!1,E=!1;for(const _ of m){const{view:N,reviews:T}=_,{failing:C,pending:M}=sa(_,{pr:_.number,since:w,me:null}),L=Ui({...N,checks:{state:C.length?"fail":M?"pending":"pass",failing:C,pending:M}},{needsApproval:h}),ke=T.filter(ie=>ie.state==="APPROVED").sort((ie,be)=>String(ie.submitted_at).localeCompare(String(be.submitted_at))).at(-1)?.commit_id;let me=!!(ke&&N.headRefOid&&ke!==N.headRefOid);me&&n===void 0&&tr(s,ke,N.headRefOid)&&(me=!1,E=!0),v||=me,me&&n===void 0&&L.push('commits after the approval: pass --after-approval "<why they are trivial>" when they are, else ask the approver to look again'),k.push(...u.length>1?L.map(ie=>`${We([_],l)} ${ie}`):L)}m.length||k.push(u.length>1?"every PR is closed":"PR is closed"),n!==void 0&&!v&&d("--after-approval: no commit came after the approval; drop it");const O=s.manual_steps?.length?a===void 0?null:await Ya(e,a,"--confirmed"):void 0;if(a!==void 0&&O===void 0&&d(`--confirmed: ${t} has no manual steps; drop it`),O===null&&k.push(`manual steps: merge only on the owner's word, with --confirmed "<their words>"`),k.length){const _=`Not merged: ${k.join("; ")}.`;i&&await U(e,t,{text:_}),d(_)}const B=[];for(const _ of m){const N=await new Promise(T=>{ea("gh",["pr","merge",String(_.number),"--repo",_.repo,"--squash"],{encoding:"utf8",timeout:12e4},(C,M,L)=>T(C?String(L||C.message).trim().split(`
25
+ `)[0]:null))});N&&d(`gh pr merge ${We([_],l)} failed: ${N}${B.length?` (merged ${We(B,l)} before it)`:""}`),B.push(_)}const S=[ni(B,l),n!==void 0&&`After the approval: ${j(n,"--after-approval")}`,E&&`After the approval: only the board's rebase onto ${s.rebased.at(-1).base}.`,O&&`The owner's word on the manual steps: "${O}"`];return we(e,t,{merged:B,text:S.filter(Boolean).join(" ")})}r(nl,"mergeCard");async function al(e,t){return(await x(e,t)).item.merge_ready||null}r(al,"heldMerge");async function il(e,t){const{source:n,wave:a,item:i}=await x(e,t);(!i.merge_ready||G(i))&&d(`${t} isn't waiting for its Merge`);const o=await Mt(e,t,i);return o&&d(`${t} is blocked ${o}, so its Merge waits until nothing blocks it`),i.merge_ready.tapped=new Date().toISOString(),await y(n.file,a),i.merge_ready}r(il,"tapMerge");async function ol(e,t){const{source:n,item:a}=await x(e,t);return(await Promise.all((await z(n.dir,a.id)).map(o=>$(o,null)))).filter(o=>o?.worktree?.path).sort((o,s)=>String(s.started).localeCompare(String(o.started)))[0]?.model}r(ol,"builderModel");async function Tn(e,t,n,a,i,o,s,c){try{const l=await ee(e),u=dn(l.tickets||{source:"internal"}),w=r((m,h)=>m.internal===!0?String(h.id).replace(/^S(\d+)$/,`${u.prefix}-$1`):null,"ownId");i===void 0&&u.source==="internal"&&un(u,w(n,a))&&(i=w(n,a));let p=0;if(u.source==="internal")for(const m of await F(e)){const h=await $(m.file,{});for(const k of h.items||[]){const v=[k.ticket,w(h,k)];if(p=Math.max(p,...v.map(E=>un(u,E)||0)),i!==void 0&&`${m.slug}/${k.id}`!==t&&v.includes(i))throw Object.assign(new ae(`${i} is already the ticket of ${m.slug}/${k.id}`),{held:!0})}}c??=i===void 0&&u.source!=="internal"&&a.plan?await P.readFile(await Wt(s,a.plan),"utf8"):a.notes||"";const f=await io(u,{ticket:i,title:a.title,body:c,repo:te(e,n),next:p+1},o);a.ticket=f.ticket,f.created&&u.source!=="internal"?a.ticket_created=!0:delete a.ticket_created}catch(l){if(l instanceof ae)throw l;d(l.message)}}r(Tn,"assignTicket");const Be=r((e,t)=>at(g.join(e||At(),"tickets"),t,{heartbeat:!0}),"ticketWrite"),we=r((e,t,n,a)=>n.ticket!==void 0||n.fromAsk!==void 0?Be(e,()=>ai(e,t,n,a)):ai(e,t,n,a),"setItem"),sl=r((e,t,n,a)=>Be(e,()=>zs(e,t,n,a)),"addItem");async function zs(e,t,{title:n,notes:a,notesFrom:i,deps:o,ticket:s,pickUpAt:c,parent:l,size:u,model:w,state:p,relate:f,intake:m=!1},h){const{wave:k,id:v}=t.includes("/")?R(t):{wave:t};v!==void 0&&ge(v,"the item id"),Bn(p),p==="ready"&&d("item add files a card grilling or prototyping; it reaches ready through staging, on the owner's Send, Implement or Reply");const{source:E,wave:O}=await Ne(e,k);v!==void 0&&O.items.some(C=>C.id===v)&&d(`wave "${k}" already has an item "${v}"`);const B=new Date().toISOString(),S={id:v,title:j(n,"--title"),created:B,lane:"todo",lane_at:B};l!==void 0&&(S.parent=await li(e,k,null,l)||d("--parent takes a card, such as <wave>/<id>")),a!==void 0&&i!==void 0&&d("pass --notes or --notes-from, not both");const _=i!==void 0?await Ua(e,i,"--notes-from",()=>`${Nn}, and link that card with --relate`):a===void 0?"":j(a,"--notes");i!==void 0&&!_.trim()&&d(`--notes-from ${i} is empty`);const N=i!==void 0?_:`# ${S.title}
26
+ ${_&&`
27
+ ${_}
28
+ `}`;ii(S,{size:u,model:w}),o!==void 0&&(S.deps=await ci(e,k,o)),c!==void 0&&Ut(S,{until:oi(c)}),f!==void 0&&(S.related=await Ht(e,t,f,"--relate")),p!==void 0?S.state=p:m&&e&&(S.unfiled=!0),await Tn(e,t,O,S,s,h,E.dir,_),v===void 0&&(S.id=co(S.ticket),O.items.some(C=>C.id===S.id)&&d(`wave "${k}" already has an item "${S.id}"; link ${S.ticket} with item add ${k}/<id> --ticket ${S.ticket}`)),S.plan=Dn(E.dir,S.id),await Rn(g.join(E.dir,S.plan),N);const T=`${k}/${S.id}`;return O.items.push(S),await y(E.file,O),S.unfiled&&await se(fe().find(C=>he(C.dir,e))||{dir:e},"card-added",{key:T,title:S.title}),e&&await ui(e,T,{related:new Set,parent:null},await Cn(e,T)),{file:E.file,key:T,lines:[]}}r(zs,"createItem");const rl=r((e,{dryRun:t=!1}={},n)=>Be(A(e),()=>Ks(e,t,n)),"backfillTickets");async function Ks(e,t,n){const a=await fi(e)||d("this board has no ticket source yet; pick one in Board settings first"),i=new Set,o=[];for(const w of await F(e)){const p=await $(w.file,null);if(Array.isArray(p?.items)){o.push({entry:w,wave:p});for(const f of p.items)f.ticket&&i.add(f.ticket),p.internal===!0&&a.source==="internal"&&i.add(String(f.id).replace(/^S(\d+)$/,`${a.prefix}-$1`))}}const s=[],c={linked:0,created:0,failed:0},l=r((w,p)=>p.ticket&&!(a.source!=="internal"&&w.internal===!0&&p.ticket===p.id),"ticketed");for(const{entry:w,wave:p}of o)for(const f of p.items){if(l(p,f)||G(f))continue;const m=`${w.slug}/${f.id}`,h=[...ro(a,f.title),...(f.linear||[]).filter(k=>un(a,k))].find(k=>a.source!=="internal"||!i.has(k));if(t){c[h?"linked":"created"]++,s.push(`${m} would ${h?`link ${h}`:"get a new ticket"}`);continue}try{await Tn(e,m,p,f,h,n,w.dir),await y(w.file,p),i.add(f.ticket),c[h?"linked":"created"]++,s.push(`${m} ${h?"linked":"created"} ${f.ticket}`)}catch(k){if(!(k instanceof ae))throw k;c.failed++,s.push(`${m} failed: ${k.message}`)}}const u=t?`would link ${c.linked}, create ${c.created}`:`linked ${c.linked}, created ${c.created}, failed ${c.failed}`;return{lines:[...s,`backfill${t?" (dry run)":""}: ${u}`]}}r(Ks,"backfill");const cl=r(e=>Be(e,async()=>{const t=[];for(const n of await F(e)){const a=await $(n.file,null);if(!Array.isArray(a?.items)||!Array.isArray(a.followups))continue;const i=a.followups.find(l=>a.items.some(u=>u.id===l.id));i&&d(`${n.slug}: follow-up ${i.id} has the id of a card; rename one, then restart the board`);const o=[];for(const{id:l,title:u,from:w,filed:p,done:f,...m}of a.followups){const h=f?e&&(await K(W(e,`${n.slug}/${l}`))).map(k=>k.ts).filter(Boolean).sort().pop()||p:null;o.push({id:l,title:u,...p?{created:p}:{},...w?{parent:H(n.slug,w)}:{},...f?{lane:"done",...h?{lane_at:h,closed_at:h}:{}}:{},...m})}const{followups:s,...c}=a;await y(n.file,{...c,items:[...a.items,...o]}),t.push(`${n.slug}: ${o.length} follow-ups moved to cards (${o.filter(l=>l.lane==="done").length} done)`)}return t}),"migrateFollowups");function pi(e,t=[]){if(e.status===void 0&&e.status_at===void 0&&e.merged_at===void 0)return!1;const n=e.merged_at||e.status_at||new Date().toISOString();["merged","done","dropped"].includes(e.status)&&Object.assign(e,{lane:"done",lane_at:n,closed_at:n}),(e.status==="merged"||e.merged_at)&&(e.prs=st(e).map(i=>({...i,merged_at:i.merged_at||n})),delete e.pr),e.status==="held"&&(e.held=!0);const a=t.filter(i=>i.started&&tt(i)).sort((i,o)=>String(i.started).localeCompare(String(o.started))).at(-1);return e.status==="checking"&&a&&(a.handed_back=n),delete e.status,delete e.status_at,delete e.merged_at,!0}r(pi,"statusToFacts");const ll=r(e=>Be(e,async()=>{const t=[];for(const n of await F(e)){const a=await $(n.file,null);let i=0;for(const o of Array.isArray(a?.items)?a.items:[]){const s=o.status==="checking"?await z(n.dir,o.id):[],c=await Promise.all(s.map(l=>$(l,null)));if(pi(o,c.filter(Boolean))){i++;for(const[l,u]of c.entries())u?.handed_back&&await y(s[l],u)}}i&&(await y(n.file,a),t.push(`${n.slug}: ${i} cards' statuses became lanes and facts`))}return await Kt(e,async()=>{const n=await Ve(e),a=n.filter(i=>i.item&&pi(i.item)).length;a&&(await Gt(e,n),t.push(`archive: ${a} cards' statuses became lanes and facts`))}),t}),"migrateStatuses"),dl=r(e=>Be(e,async()=>{const t=[];for(const n of await F(e)){const a=await $(n.file,null),i=(Array.isArray(a?.items)?a.items:[]).filter(o=>o.implement_at);if(i.length){for(const o of i)o.state!=="ready"&&Object.assign(o,{state:"staging",staged_at:o.implement_at}),delete o.implement_at;await y(n.file,a),t.push(`${n.slug}: ${i.length} Implement asks moved to staging or ready`)}}return t}),"stageImplementAsks"),zt=r(e=>g.join(e,"data","done-archive.json"),"archiveFile"),Kt=r((e,t)=>at(g.join(e,"done-archive"),t,{heartbeat:!0}),"archiveWrite"),Ve=r(async e=>(await $(zt(e),{cards:[]})).cards||[],"readArchive"),Jt=r(e=>`${e.closed_at||""}|${e.key}`,"archiveRank"),Js=r((e,t)=>Jt(e)<Jt(t)?1:Jt(e)>Jt(t)?-1:0,"byClosed"),qn=r(e=>g.join(e,"data","done-archive-count.json"),"archiveCountFile");async function ul(e){const t=(await $(qn(e),null))?.count;if(Number.isInteger(t))return t;const n=(await Ve(e)).length;return await y(qn(e),{count:n}),n}r(ul,"archiveCount");async function Gt(e,t){t.length?await y(zt(e),{cards:t.sort(Js)}):await P.rm(zt(e),{force:!0}),await y(qn(e),{count:t.length})}r(Gt,"writeArchive");const fl=r((e,t)=>Kt(e,async()=>{const n=new Map(t.map(p=>[p.key,p])),a=new Date().toISOString(),i=[],o=[];for(const p of await F(e)){const f=await $(p.file,null);if(!Array.isArray(f?.items))continue;const m=f.items.filter(h=>n.has(`${p.slug}/${h.id}`)&&h.lane==="done");if(m.length){for(const h of m){const k=n.get(`${p.slug}/${h.id}`);i.push({...k,wave:p.slug,archived_at:a,item:k.prs?Gs(h,k.prs):h})}o.push([p.file,{...f,items:f.items.filter(h=>!m.includes(h))}])}}if(!i.length)return[];const s=await $(I(e),{items:[]}),c=new Map(i.map(p=>[`item:${p.key}`,p]));for(const p of i)p.questions=(s.items||[]).filter(f=>f.topic===`item:${p.key}`&&f.resolved);const l=await Ie(e);for(const p of i)p.asks=p.item.from_ask&&V(l,p.item.from_ask)===void 0?[p.item.from_ask]:[];const u=new Set(i.map(p=>p.key));await Gt(e,[...(await Ve(e)).filter(p=>!u.has(p.key)),...i.map(({key:p,wave:f,archived_at:m,closed_at:h,item:k,questions:v,asks:E})=>({key:p,wave:f,archived_at:m,closed_at:h,item:k,questions:v,asks:E}))]),i.some(p=>p.asks.length)&&await y(_e(e),{links:{...l,...Object.fromEntries(i.flatMap(p=>p.asks.map(f=>[f,p.key])))}});for(const[p,f]of o)await y(p,f);const w=(s.items||[]).filter(p=>!(c.has(p.topic)&&p.resolved));return w.length!==(s.items||[]).length&&await y(I(e),{...s,items:w}),[...u]}),"archiveCards");function Gs(e,t){if(!Re(e))return e;const n=st(e).map(o=>{const s=t.find(c=>c.state&&c.number===o.number&&(!o.repo||o.repo===c.repo));return s?{...o,kept:{checks:s.checks,reviews:s.reviews,codexOpen:s.codexOpen}}:o}),{pr:a,...i}=e;return{...i,prs:n}}r(Gs,"keptPRs");async function Qs(e,t){return!e||!de(zt(e))?!1:Kt(e,async()=>{const n=await Ve(e),a=n.find(p=>p.key===t);if(!a)return!1;const i=(await F(e)).find(p=>p.slug===a.wave),o=i&&await $(i.file,null);if(!Array.isArray(o?.items))return!1;o.items.some(p=>p.id===a.item.id)||await y(i.file,{...o,items:[...o.items,a.item]});const s=await $(I(e),{items:[]}),c=new Set((s.items||[]).map(p=>p.id)),l=(a.questions||[]).filter(p=>!c.has(p.id));l.length&&await y(I(e),{...s,items:[...s.items||[],...l]});const u=await Ie(e),w=(a.asks||[]).filter(p=>V(u,p)===t);return w.length&&await y(_e(e),{links:Object.fromEntries(Object.entries(u).filter(([p])=>!w.includes(p)))}),await Gt(e,n.filter(p=>p!==a)),!0})}r(Qs,"restoreArchived");const wi=["push_tag","no_change"],mi=r(e=>Object.fromEntries(Object.entries(e).filter(([t])=>!wi.includes(t))),"stripQuestion"),hi=r(({summary:e,...t})=>t,"stripItem"),gi=r(e=>wi.some(t=>t in e),"holdsDead"),pl=r(e=>Kt(e,async()=>{for(const a of await F(e)){const i=await $(a.file,null);Array.isArray(i?.items)&&i.items.some(o=>"summary"in o)&&await y(a.file,{...i,items:i.items.map(hi)})}const t=await $(I(e),null);(t?.items||[]).some(gi)&&await y(I(e),{...t,items:t.items.map(mi)});const n=await Ve(e);n.some(a=>"card"in a||"summary"in a.item||(a.questions||[]).some(gi))&&await Gt(e,n.map(({card:a,...i})=>({...i,...a?{closed_at:i.closed_at||a.sortAt}:{},item:hi(i.item),questions:(i.questions||[]).map(mi)})))}),"stripDeadFields"),$i=r(e=>`lineup-${e}.md`,"lineupNoteFile"),Zs=r(e=>typeof e=="string"&&/^[a-z0-9-]+$/.test(e)?e:d(`a wave's slug uses only a-z, 0-9 and "-", not "${e}"`),"lineupSlug");async function Qt(e,t){for(const n of await F(e)){const a=await $(n.file,null),i=(a?.lineups||[]).find(o=>o.slug===t);if(i)return{source:n,wave:a,lineup:i}}return d(`this board has no wave "${t}"; lineup create makes one`)}r(Qt,"findLineup");async function Vs(e,t){(await ee(e)).items!=="waves"&&d(`topic wave:${t} is for a board over ~/.claude/waves; a lineup's triage uses lineup:<slug>`),await Ne(e,t)}r(Vs,"triagedWave");function wt(e,t,n,a){return n.map(i=>{const{wave:o,id:s}=R(i);o!==e.slug&&d(`${i} is not in wave file "${e.slug}"; a wave's cards share one file`),t.items.some(l=>l.id===s)||d(`wave "${o}" has no item "${s}"`);const c=(t.lineups||[]).find(l=>l.slug!==a&&!l.finished_at&&an(l).flat().includes(s));return c&&d(`${i} is already in the wave "${c.slug}"; lineup remove ${c.slug} ${i} first`),s})}r(wt,"lineupCards");function ki(e){const t=[e??[]].flat().map(n=>String(n).split(",").map(a=>a.trim()).filter(Boolean));return(!t.length||t.some(n=>!n.length))&&d("--phase takes one phase's cards, as <wave>/<id>,<wave>/<id>, once per phase in build order"),t}r(ki,"phaseKeys");function bi(e,t,n,a){const i=new Set(n.map(o=>`${e.slug}/${o}`));for(const o of n){const s=t.items.find(u=>u.id===o),c=(s.deps||[]).filter(u=>!i.has(H(e.slug,u))),l=a.findIndex(u=>u.includes(o));l>0&&c.push(...a[l-1]),c.length?s.deps=c:delete s.deps}}r(bi,"chain");async function yi(e,t,n,a,i){i!==void 0&&await rt(g.join(t.dir,$i(a.slug)),`${j(i,"the note").replace(/\n*$/,`
29
+ `)}`),await y(t.file,n);const{tickets:o}=$e(await ee(e));return{file:t.file,lines:[`order: ${xi(n,a.phases,o)}`]}}r(yi,"writeLineup");const xi=r((e,t,n)=>t.map(a=>a.map(i=>Me(e,e.items.find(o=>o.id===i)||{id:i},n)).join(", ")).join(" \u2192 "),"orderLine");async function wl(e,t,{title:n,goal:a,phases:i,note:o}){Zs(t);const s=ki(i);o===void 0&&d("lineup create needs the wave note, as --note <text> or --file <path>");const c=await F(e);c.some(f=>f.slug===t)&&d(`"${t}" is a wave file on this board; pick another slug`);for(const f of c)((await $(f.file,null))?.lineups||[]).some(m=>m.slug===t)&&d(`this board already has a wave "${t}"`);const l=c.find(f=>f.slug===R(s[0][0]).wave)||d(`this board has no wave "${R(s[0][0]).wave}"`),{wave:u}=await Ne(e,l.slug),w=s.map(f=>wt(l,u,f,t));new Set(w.flat()).size!==w.flat().length&&d("--phase names a card twice");const p={slug:t,title:j(n,"--title"),goal:j(a,"--goal"),phases:w,created:new Date().toISOString()};return bi(l,u,w.flat(),w),u.lineups=[...u.lineups||[],p],yi(e,l,u,p,o)}r(wl,"createLineup");async function ml(e,t,{add:n,after:a,remove:i,phases:o,title:s,goal:c,note:l}){[n,i,o,s,c,l].every(m=>m===void 0)&&d("nothing to change: pass --add, --remove, --phase, --title, --goal, --note or --file"),a!==void 0&&n===void 0&&d("--after goes with --add");const{source:u,wave:w,lineup:p}=await Qt(e,t);p.finished_at&&d(`the wave "${t}" is finished`),p.phases=an(p),delete p.items;const f=p.phases.flat();if(n!==void 0){const[m]=wt(u,w,[n],t);f.includes(m)&&d(`${n} is already in the wave "${t}"`);const h=a===void 0?p.phases.length:p.phases.findIndex(k=>k.includes(wt(u,w,[a],t)[0]))+1;h===0&&d(`--after ${a} is not in the wave "${t}"`),p.phases.splice(h,0,[m])}if(i!==void 0){const[m]=wt(u,w,[i],t);f.includes(m)||d(`${i} is not in the wave "${t}"`),f.length===1&&d(`${i} is the wave's last card; lineup finish ${t} ends the wave`),p.phases=p.phases.map(h=>h.filter(k=>k!==m)).filter(h=>h.length)}if(o!==void 0){const m=ki(o).map(k=>wt(u,w,k,t)),h=m.flat();(h.length!==f.length||new Set(h).size!==h.length||h.some(k=>!f.includes(k)))&&d("--phase lists each of the wave's cards once"),p.phases=m}return s!==void 0&&(p.title=j(s,"--title")),c!==void 0&&(p.goal=j(c,"--goal")),bi(u,w,[...new Set([...f,...p.phases.flat()])],p.phases),yi(e,u,w,p,l)}r(ml,"changeLineup");async function hl(e,t){const{source:n,wave:a,lineup:i}=await Qt(e,t);return i.finished_at=new Date().toISOString(),await y(n.file,a),{file:n.file}}r(hl,"finishLineup");async function gl(e,t,n){const{source:a,wave:i,lineup:o}=await Qt(e,t),s=await P.readFile(g.join(a.dir,$i(t)),"utf8").catch(()=>"");return{title:o.title,markdown:`**Goal** ${o.goal}
19
30
 
20
- **Order** ${Ai(a,At(s),i)}
31
+ **Order** ${xi(i,an(o),n)}
21
32
 
22
- ${o}`}}r(Yr,"readLineupNotes");async function K(e,t,i,n="main"){const{id:a}=C(t);/^[\w-]{1,40}$/.test(n)||u(`--agent takes a name of letters, digits, "_" and "-", not "${n}"`);const s=n==="main"?`${a}.json`:`${a}@${n}.json`;if(i)return h.join(h.resolve(i),"agents",s);const{source:o}=await v(e,t);return h.join(o.dir,"agents",s)}r(K,"builderFile");const Ri=r((e,t)=>typeof e=="string"&&/^\d{4}-\d\d-\d\dT/.test(e)&&!Number.isNaN(Date.parse(e))?new Date(e).toISOString():u(`${t} takes an ISO time such as 2026-10-23T16:30:00-07:00`),"isoTime"),Ks=["reading","building","testing","checking","landing"],Ni=80,Ei="reading, building, testing, checking or landing";function Li(e,t,i,n){e||u(`${n} needs --site, since the line goes to the item's thread`),t!==void 0&&!Ks.includes(t)&&u(`--phase takes ${Ei}, not "${t}"`);const a=j(i,n);return a.length>Ni&&u(`${n} is ${a.length} characters; the cap is ${Ni}. Say it shorter.`),{phase:t,detail:a}}r(Li,"progressLine");const Ci=r((e,t,i)=>t?{detail:e.detail}:(e.phase===void 0&&u(`${i} needs --phase ${Ei}`),e),"forAgent");async function nn(e,t,i="main",n,{phase:a,detail:s},o){const c=Re(n),d=c||i==="main"?"":et(n)||n.kind||i,f=c?Yi[n.kind]:[d,a].filter(Boolean).join(" "),p=new RegExp(`^(?:${f}|${a??f})\\s*:?\\s+`,"i"),l=f?`${f[0].toUpperCase()}${f.slice(1)}: ${s.replace(p,"")}`:s,w=U(e,t);(await G(w)).filter(m=>m.kind==="status").pop()?.text!==l&&await Y(w,{ts:o,who:"claude",kind:"status",...a?{phase:a}:{},...c?{planner:!0}:{},text:l})}r(nn,"statusToThread");const Hs={prototype:"sonnet",grill:"opus"};async function an(e,t,{label:i,phase:n,progress:a,dir:s,agent:o="main",kind:c,model:d,eta:f,worktree:p,ownWorktree:l,dispatch:w}){n!==void 0&&a===void 0&&u('--phase goes with --progress "<detail>"');const m=z(a,T=>Li(e,n,T,"--progress")),g=await K(e,t,s,o),b=new Date().toISOString(),P=z(c,T=>/^[\w.-]{1,30}$/.test(T)?T:u("--kind takes a short word such as claude or codex")),D=Be(P),S=d??(P==="opus"||P==="sonnet"?P:void 0),_=["build",...Ys].includes(D),x=await $(g,null),O=r(T=>["tap","hub"].includes(T?.dispatched)&&!T.done,"tapOpen");if(w==="session"&&O(x))return{file:g,alreadyOpen:!0,lines:[]};o!=="main"&&O(await $(await K(e,t,s),null))&&u(`the Implement tap opened ${t}'s main builder record and no agent has taken it up; run builder start ${t} without --agent first`);const R=x?.dispatched&&!x.done?x:null,Z=_&&!s?(await v(e,t)).item:void 0,ne=x&&Oe(x).length?x:null,F=R&&D==="build"?{kind:Be(R.kind),model:R.model,reason:{tap:"The owner's Build tap picked it.",hub:"The board's pull picked it."}[R.dispatched]??"The session's dispatch picked it."}:_?mn({card:Z??{}}):null;F&&D!=="build"&&D!==F.kind&&u(`--kind ${D}: ${F.kind} builds now. ${F.reason} Pass --kind ${F.kind}, or --kind build.`);const B=F?F.kind:D,oe=wn();if(ia(oe,B))throw new Q(na(oe,B));const ie=m&&Ci(m,Re({kind:B}),"--progress");d!==void 0&&!["opus","sonnet"].includes(d)&&u("--model takes opus or sonnet"),S!==void 0&&B==="codex"&&u("--model is for kind claude; codex needs none");const ce=B==="claude"?ca():"auto";S!==void 0&&ce!=="auto"&&S!==ce&&u(`--model ${S}: the hub's Claude model is ${ce==="opus"?"Opus":"Sonnet"}. Pass --model ${ce}, or no --model.`);const ae=B==="claude"?S??F?.model??(R&&Be(R.kind)==="claude"?R.model:void 0)??"opus":Re({kind:B})?S??(R?.kind===B?R.model:void 0)??Hs[B]:void 0,le={label:i===void 0?"Builder":j(i,"--label"),...B?{kind:B}:{},...ae?{model:ae}:{},started:b,...f===void 0?{}:{eta:Ri(f,"--eta")},...w?{dispatched:w}:{},...ne?{calls:ne.calls,...ne.verified?{verified:ne.verified}:{}}:{},...Qs(x)?{session:x.session}:{}},de=_&&!s&&![...I,"building","fixing"].includes(Z.status),vt=de?await se(e,t,{status:"building"}):{lines:[]};let V,ue=null;const ye=l&&_&&o==="main"?[x?.worktree,Z?.worktree].find(T=>T?.path&&De(T.path))??null:null,Pe=(R??ne)?.worktree??ye,be=p??(_&&o==="main"&&!Pe?l:void 0);try{if(be){const T=await te(h.dirname(h.dirname(g)),C(t).id),xe=(await Promise.all(T.filter(k=>k!==g).map(k=>$(k,null)))).filter(k=>k?.worktree?.path&&!k.done).map(k=>k.worktree.path);({remade:ue,...V}=await be({busy:xe}))}else V=Pe}catch(T){throw de&&await Vs(e,t,Z),T}await y(g,V?{...le,worktree:V}:le),Z?.rebase_run&&o==="main"&&![Ii,"Lander"].includes(le.label)&&await Bi(e,t),F?.inputs&&e&&ba(hn(e),{call:"builder-pick",key:t,result:[B,B==="claude"?ae:null].filter(Boolean).join(" "),reason:F.reason,inputs:{...F.inputs,...S?{askedModel:S}:{},...w?{dispatch:w}:{}}}),ue&&e&&await re(e,t,{text:ue}),ie&&await nn(e,t,o,le,ie,b);const H=await Vt(e,t,"Acted on: builder started");return e&&_&&await _s(e,t),{file:g,kind:B??null,model:ae??null,reason:F?.reason??null,worktree:V?.path??null,lines:[...D==="build"?[`kind: ${B}. ${F.reason}`]:[],...et(le)?[`model: ${et(le)}`]:[],...e?[`ref: ${await X(e,t)}`]:[],...V?[`worktree: ${V.path}`]:[],...ue?[ue]:[],...vt.lines,...H]}}r(an,"startBuilder");async function Js(e,t,i,n="tap"){try{const{held:a,blockers:s}=await Gs(e,t);if(a&&await hi(e,"card-hold",{key:t,hold:!1}),s.length)return await Ti(e,t,s);try{const o=await an(e,t,{kind:"build",worktree:i,dispatch:n});return{key:t,ref:await X(e,t),kind:o.kind,...o.model?{model:o.model}:{},reason:o.reason,worktree:o.worktree}}catch(o){throw a&&await Zs(e,t,a),o}}catch(a){if(!(a instanceof Q))throw a;return await Y(U(e,t),{ts:new Date().toISOString(),who:"claude",kind:Dt,text:`Couldn't start implementing: ${a.message}`}),null}}r(Js,"startTappedBuild");async function Xr(e,t,{pr:i,conflict:n,base:a},s){const{source:o,wave:c,item:d}=await v(e,t);d.rebase_run={pr:i,base:a,conflict:n,at:new Date().toISOString()},await y(o.file,c);try{const f=await an(e,t,{kind:"build",label:Ii,dispatch:"hub",ownWorktree:s});return{key:t,kind:f.kind,...f.model?{model:f.model}:{},worktree:f.worktree}}catch(f){throw await Bi(e,t),f}}r(Xr,"startRebase");const Ii="Rebase";async function Bi(e,t){const{source:i,wave:n,item:a}=await v(e,t);delete a.rebase_run,await y(i.file,n)}r(Bi,"endRebaseRun");async function ec(e,t){const{wave:i,item:n}=await v(e,t),a=ee(e,i);return Ne(n,a).find(s=>s.repo===a)?.number??null}r(ec,"cardPR");async function tc(e,t,{from:i,to:n,replayed:a}){const{source:s,wave:o,item:c}=await v(e,t);return c.rebase_run?(c.rebased=[...c.rebased||[],{from:i,to:n,base:c.rebase_run.base,replayed:a,at:new Date().toISOString()}],delete c.rebase_run,await y(s.file,o),!0):!1}r(tc,"recordRebase");const zs=r((e,t,i)=>{const n=new Map((e.rebased||[]).filter(a=>a.replayed).map(a=>[a.to,a.from]));for(let a=i,s=0;a&&s<=n.size;a=n.get(a),s++)if(a===t)return!0;return!1},"onlyRebased");async function Gs(e,t){const i=wn();i.state==="paused"&&u(ta(i));const{item:n}=await v(e,t),a=n.held||n.status==="held"?{held:n.held,status:n.status,pick_up_at:n.pick_up_at,held_at:n.held_at}:null;a&&![...Tn,"held"].includes(n.status)&&u("it's held. Unhold it, then tap Implement."),await oi(e,t)&&u("no take is picked on its prototypes, so it prototypes again. Pick a take from the next round, then tap Implement.");const s=await Promise.all((await ze(e,C(t).wave,n)).map(o=>X(e,o)));return{held:a,blockers:s}}r(Gs,"tapRefusals");async function nc(e,t){const{item:i}=await v(e,t);if(i.implement_at)return;(i.held||i.status==="held")&&await hi(e,"card-hold",{key:t,hold:!1});const{source:n,wave:a,item:s}=await v(e,t);s.implement_at=new Date().toISOString(),await y(n.file,a)}r(nc,"askImplement");async function Ti(e,t,i){const n=i.join(", ");return await pe({dir:e},"card-bump",{key:t}),await Y(U(e,t),{ts:new Date().toISOString(),who:"claude",text:`Can\u2019t start yet: waits on ${n}. Bumped to the top of Todo.`}),{key:t,ref:await X(e,t),bumped:i,line:`bumped: ${t} waits on ${n}, so it stays in Todo, bumped to the top`}}r(Ti,"bumpBlocked");async function ic(e,t,i){let n;try{n=await Js(e,t,i)}catch(a){await Y(U(e,t),{ts:new Date().toISOString(),who:"claude",kind:Dt,text:`Couldn't start implementing: ${a.message}`}),n=null}if(!n){const{source:a,wave:s,item:o}=await v(e,t);delete o.implement_at,await y(a.file,s)}return n}r(ic,"startReleasedBuild");async function ac(e,t,i){const n=await K(e,t),a=await $(n,null),s=new Date().toISOString();a&&!a.done&&await y(n,{...a,done:s});const{source:o,wave:c,item:d}=await v(e,t);d.status==="building"&&(delete d.status,d.status_at=s),await y(o.file,c),await Y(U(e,t),{ts:s,who:"claude",kind:Dt,text:`Couldn't start implementing: ${i}`})}r(ac,"abandonLaunch");async function sc(e,t,i){const n=await K(e,t),a=await $(n,null);a&&!a.done&&await y(n,{...a,done:new Date().toISOString()}),await mt(e,t),await re(e,t,{text:i})}r(sc,"abandonPlanner");async function oc(e,t,i,n){const a=await K(e,t),s=await $(a,null);await y(a,{...s,session:{id:i,pid:n,at:new Date().toISOString()}})}r(oc,"recordSession");async function rc(e,t){const{source:i}=await v(e,t);return(await Promise.all((await te(i.dir,C(t).id)).map(a=>$(a,null)))).filter(a=>a&&a.started&&!a.done).map(a=>({pid:a.session?.pid??null,at:a.session?.at??null}))}r(rc,"openAgents");async function cc(e,t){return(await G(U(e,t))).filter(i=>i.kind==="status").pop()?.phase==="landing"}r(cc,"isLanding");const Qs=r(e=>!!e?.session&&e.session.at>(e.done??e.started),"launchedSession");async function dc(e,t,i){const n=await $(await K(e,t),null);if(n&&!n.done)return null;const a=await an(e,t,{kind:i,dispatch:"hub"});return{key:t,kind:i,model:a.model}}r(dc,"startPlanner");async function lc(e){const t=[];for(const i of await q(e)){const n=await $(i.file,null);for(const a of n?.items||[]){if(a.status!=="building")continue;const s=await $(h.join(i.dir,"agents",`${a.id}.json`),null);!s?.started||s.done||!s.dispatched||Re(s)||t.push({key:`${i.slug}/${a.id}`,kind:Be(s.kind),model:s.model})}}return t}r(lc,"cutOffBuilds");async function Zs(e,t,i){const{source:n,wave:a,item:s}=await v(e,t);for(const o of Object.keys(i))i[o]===void 0?delete s[o]:s[o]=i[o];await y(n.file,a)}r(Zs,"restoreHold");async function Vs(e,t,i){const{source:n,wave:a,item:s}=await v(e,t);for(const o of["status","status_at"])i[o]===void 0?delete s[o]:s[o]=i[o];await y(n.file,a)}r(Vs,"restoreStatus");async function uc(e,t){return e||u("audit reads a board: pass --site <route>"),t!==void 0&&C(t),{lines:xa(va(hn(e),{key:t}))}}r(uc,"auditLog");const Ys=["codex","claude"];async function fc(e,t){const{item:i}=await v(e,t),{kind:n,model:a,reason:s}=mn({card:i});return{lines:[`${[n,a].filter(Boolean).join(" ")}: ${s}`]}}r(fc,"pickBuilder");async function pc(e,t,i){const{source:n,wave:a,item:s}=await v(e,t);if(s.worktree?.path&&De(s.worktree.path))return null;const o=await sn(e,t);if(o?.path&&De(o.path))return null;const{remade:c,...d}=await i({busy:[]});return s.worktree=d,await y(n.file,a),d}r(pc,"ensureWorktree");async function sn(e,t,{dir:i}={}){const{id:n}=C(t),a=h.dirname(await K(e,t,i)),s=await te(h.dirname(a),n);return(await Promise.all(s.map(c=>$(c,null)))).filter(c=>c?.worktree?.path).sort((c,d)=>String(d.started).localeCompare(String(c.started)))[0]?.worktree??null}r(sn,"recordedWorktree");async function wc(e,t,i,{phase:n,dir:a,agent:s="main",eta:o,clearEta:c}){o!==void 0&&c&&u("pass --eta or --clear-eta, not both");const d=Li(e,n,i,"builder progress"),f=await K(e,t,a,s),p=await $(f,null);(!p?.started||p.done)&&u(`builder progress needs an open builder record on ${t}; run builder start first`);const l=Ci(d,Re(p),"builder progress");return o!==void 0&&(p.eta=Ri(o,"--eta")),c&&delete p.eta,(o!==void 0||c)&&await y(f,p),await nn(e,t,s,p,l,new Date().toISOString()),f}r(wc,"builderProgress");async function mc(e,t){await v(e,t);const i=U(e,t);(await G(i)).filter(n=>n.kind==="status").pop()?.phase!=="landing"&&await Y(i,{ts:new Date().toISOString(),who:"claude",kind:"status",phase:"landing",text:"Landing: rebasing onto main and landing"})}r(mc,"landingLine");const Oe=r(e=>(e?.calls||[]).filter(t=>!e.verified||t.at>e.verified.at),"unverifiedCalls");async function hc(e,t,i,{dir:n,agent:a="main"}){const s=j(i,"builder call");/\n/.test(s)&&u("builder call takes one line; record each call on its own");const o=await K(e,t,n,a),c=await $(o,null);return(!c?.started||c.done)&&u(`builder call needs an open builder record on ${t}; run builder start first`),await y(o,{...c,calls:[...c.calls||[],{at:new Date().toISOString(),text:s}]}),o}r(hc,"recordCall");async function Ze(e,t){const i=await G(U(e,t)),n=new Map(i.filter(a=>a.kind===Xe.answer).map(a=>[a.ask,a]));return i.filter(a=>a.kind===Xe.ask).map(a=>{const s=n.get(a.ask);return{id:a.ask,agent:a.agent||"main",text:a.text,at:a.ts,answer:s?{text:s.text,at:s.ts}:null,deferred:!!s?.deferred}})}r(Ze,"builderAsks");const Xs=r(e=>we().find(t=>ve(t.dir,e))??{dir:e},"boardTarget");async function gc(e,t,i,{agent:n="main"}){A(e);const a=j(i,"builder ask");/\n/.test(a)&&u("builder ask takes one line; ask each question on its own");const s=await $(await K(e,t,void 0,n),null);(!s?.started||s.done)&&u(`builder ask needs an open builder record on ${t}; run builder start first`);const o=`ask${(await Ze(e,t)).length+1}`;return await Y(U(e,t),{ts:new Date().toISOString(),who:`${et(s)||s.kind||"Claude"} builder`,kind:Xe.ask,ask:o,agent:n,text:a}),await pe(Xs(e),"builder-ask",{key:t,ask:o,agent:n,text:a}),{id:o,lines:[]}}r(gc,"askOrchestrator");async function $c(e,t,i,n,{deferred:a=!1}={}){A(e);const s=j(n,"--text"),o=(await Ze(e,t)).find(d=>d.id===i)||u(`no ask "${i}" on ${t}; builder asks ${t} lists them`);o.answer&&u(`${i} on ${t} is already ${o.deferred?"deferred":"answered"}`);const c=new Date().toISOString();if(await Y(U(e,t),{ts:c,who:"claude",kind:Xe.answer,ask:i,...a?{deferred:!0}:{},text:s}),a){const d=await K(e,t,void 0,o.agent),f=await $(d,null);f&&await y(d,{...f,calls:[...f.calls||[],{at:c,text:`Question: ${o.text} (deferred to the owner)`}]})}return{lines:[]}}r($c,"answerBuilder");async function kc(e,t){A(e);const i=await Ze(e,t);return i.length?{lines:i.flatMap(n=>[`${n.id}. ${n.text}`,` ${n.answer?`${n.deferred?"deferred":"answered"}: ${n.answer.text}`:"waiting on the orchestrator"}`])}:{lines:["no asks yet"]}}r(kc,"listAsks");async function eo(e,t,i){const n=t.topic.startsWith("item:")&&!t.topic.startsWith("item:ask:")?t.topic.slice(5):u(`${i}.ask goes on a card's question, topic item:<wave>/<id>`),a=(await Ze(e,n)).find(s=>s.id===t.ask)||u(`${i}.ask names no ask "${t.ask}" on ${n}; builder asks ${n} lists them`);a.deferred||u(`${i}.ask names ${a.id}, which the orchestrator ${a.answer?"answered":"has not deferred"}; only a deferred ask goes to the owner`)}r(eo,"checkAskQuestion");async function yc(e,t,{dir:i,agent:n="main"}){const a=await $(await K(e,t,i,n),null),s=a?.calls||[];if(!s.length)return{lines:["no judgment calls recorded"]};const{verified:o}=a,c=Oe(a).length?"not verified yet":`verified ${o.at}${o.note?`: ${o.note}`:""}`;return{lines:[...s.map((d,f)=>`${f+1}. ${d.text}`),c]}}r(yc,"listCalls");async function bc(e,t,{dir:i,agent:n="main",note:a}){const s=await K(e,t,i,n),o=await $(s,null);if(Oe(o).length||u(`${t} has no unverified judgment calls${n==="main"?"":` from agent ${n}`}; builder calls ${t} lists them`),e){const c=new Set((await $(L(e),{items:[]})).items?.filter(f=>f.topic===`item:${t}`&&f.ask).map(f=>f.ask)),d=(await Ze(e,t)).find(f=>f.deferred&&f.agent===n&&!c.has(f.id));d&&u(`${d.id} on ${t} was deferred to the owner; add its question with "ask": "${d.id}" first`)}return await y(s,{...o,verified:{at:new Date().toISOString(),...a===void 0?{}:{note:j(a,"--note")}}}),s}r(bc,"verifyCalls");async function xc(e,t){const{source:i,item:n}=await v(e,t),a=r((o,c)=>[(o.options||[]).find(d=>d.id===c.choice)?.label,c.note].filter(Boolean).join(": ")||"answered","said"),s=(await ke(e,t)).asked.filter(o=>o.ask&&o.answers.length).map(o=>({ask:o.ask,question:o.question,answer:a(o,o.answers.at(-1))}));return{key:t,ref:await X(e,t),title:n.title||t,notes:n.plan?h.join(i.dir,n.plan):null,worktree:(await sn(e,t))?.path??null,answers:s,links:await to(e,t),rebasePR:n.rebase_run?.pr??null}}r(xc,"briefFacts");async function to(e,t){const i=new Map;for(const c of await q(e))for(const d of(await $(c.file,null))?.items||[])i.set(`${c.slug}/${d.id}`,{entry:c,it:d});const n=C(t).wave,{it:a}=i.get(t),s=[...i].filter(([,c])=>(c.it.related||[]).some(d=>W(c.entry.slug,d)===t)).map(([c])=>c),o=[...a.parent?[["parent",W(n,a.parent)]]:[],...(a.deps||[]).map(c=>["depends on",W(n,c)]),...[...new Set([...(a.related||[]).map(c=>W(n,c)),...s])].map(c=>["related",c])].filter(([,c])=>i.has(c));return Promise.all(o.map(async([c,d])=>{const{entry:f,it:p}=i.get(d);return{relation:c,ref:await X(e,d),title:p.title||d,status:p.status||"not started",notes:p.plan?h.join(f.dir,p.plan):null}}))}r(to,"cardLinks");async function vc(e){const t=[];for(const i of await q(e)){const n=await $(i.file,null);for(const a of n?.items||[]){if(a.status!=="checking")continue;const s=(await Promise.all((await te(i.dir,a.id)).map(f=>$(f,null)))).filter(Boolean),o=s.map(f=>f.verified?.at).filter(Boolean).sort().at(-1);if(!o||s.some(f=>Oe(f).length))continue;const c=`${i.slug}/${a.id}`;if((await ke(e,c)).asked.some(f=>f.state==="open"))continue;const d=s.filter(f=>f.worktree?.path).sort((f,p)=>String(p.started).localeCompare(String(f.started)))[0];d&&De(d.worktree.path)&&t.push({key:c,verified:o,kind:Be(d.kind),model:d.model})}}return t}r(vc,"landable");async function Sc(e,t){const{item:i}=await v(e,t);i.status!=="building"&&u(`${t} is ${i.status||"not started"}; only a card at building has a stopped lander to relaunch`),await se(e,t,{status:"checking",next:"A lander relaunches to land the verified build"});const n=h.join(e,"data","landers.json"),a=await $(n,{});delete a[t],await N.writeFile(n+".tmp",JSON.stringify(a)+`
23
- `),await N.rename(n+".tmp",n)}r(Sc,"relaunchLander");async function _c(e,t,{dir:i}={}){const{id:n}=C(t),a=await te(h.dirname(h.dirname(await K(e,t,i))),n),s=[];for(const o of a){if(!Oe(await $(o,null)).length)continue;const c=h.basename(o,".json").slice(String(n).length+1)||"main";s.push(c==="main"?"":` --agent ${c}`)}s.length&&u(`land: ${t} has judgment calls nobody verified yet; check them with ${s.map(o=>`builder calls ${t}${o}`).join(", ")}, then run ${s.map(o=>`builder verified ${t}${o}`).join(", ")}`)}r(_c,"checkCallsVerified");async function jc(e,t,{dir:i,agent:n="main",next:a}){const s=await $(await K(e,t,i,n),null),o=s&&!s.done?Oe(s):[];if(!o.length)return null;a!==void 0&&u(`builder done sets ${t}'s Next itself while its record has judgment calls; run it without --next`);const c=o.some(f=>f.text.startsWith("Question:"))?"Answer the builder's questions":"Verify the builder's judgment calls",d=await se(e,t,{status:"checking",next:c});return{lines:[`handed back: checking, Next "${c}"`,...d.lines]}}r(jc,"handBackCalls");async function Oc(e,t,{agent:i="main"}){if(i!=="main")return!1;const n=await K(e,t,void 0,i),a=await $(n,null);if(!a?.started||a.done||!a.worktree?.path)return!1;const{id:s}=C(t);for(const o of await te(h.dirname(h.dirname(n)),s)){const c=o===n?null:await $(o,null);if(!c?.started||c.done||c.kind==="install")continue;const d=h.basename(o,".json").slice(String(s).length+1);u(`builder done would finish ${t} while agent ${d} is still open; run builder done ${t} --agent ${d} first`)}return!0}r(Oc,"finishesCard");async function Pc(e,t){const{source:i,item:n}=await v(e,t);if(n.status!=="checking"||(await Promise.all((await te(i.dir,n.id)).map(o=>$(o,null)))).filter(Boolean).some(o=>Oe(o).length||o.started&&!o.done&&o.kind!=="install"))return null;const s=await sn(e,t);return!s||!De(s.path)?null:{waits:(await ke(e,t)).asked.filter(o=>o.state==="open").map(o=>o.id)}}r(Pc,"verifiedFinish");async function Dc(e,t){const{item:i}=await v(e,t);if(i.status!=="done"||i.merged_at)return;const n=String(i.status_at||"");(await G(h.join(e,"data","events.jsonl"))).some(s=>s.kind==="item-message"&&s.data?.key===t&&String(s.ts)>n)&&u(`the owner reopened ${await X(e,t)} with a message; it closes on the owner's Done`)}r(Dc,"checkNotReopened");const qi=["checking","done","merged","dropped"];async function te(e,t){const i=h.join(e,"agents");return(await N.readdir(i).catch(()=>[])).filter(n=>n===`${t}.json`||n.startsWith(`${t}@`)&&n.endsWith(".json")).map(n=>h.join(i,n))}r(te,"builderFiles");async function Fi(e,t,i){const n=new Date().toISOString();for(const a of await te(e,t)){const s=await $(a,null);if(!s||s.done||s.kind==="install"&&!i)continue;const{eta:o,...c}=s;await y(a,{...c,done:n})}}r(Fi,"closeBuilders");async function no(e,t,{dir:i,agent:n,startedBetween:a,note:s,next:o,requireNext:c=!1}){const d=await K(e,t,i,n),f=await $(d,null);if(!f||f.done)return d;if(a){const R=Date.parse(f.started);if(!(R>=a[0]&&R<=a[1]))return d}const{eta:p,...l}=f,w=new Date().toISOString(),m={...l,done:w,...s?{note:s}:{}},{id:g}=C(t),b=h.dirname(h.dirname(d)),P=await Promise.all((await te(b,g)).map(R=>R===d?m:$(R,null))),D=!P.some(R=>R?.started&&!R.done),S=i?{file:h.join(b,"wave.json")}:(await v(e,t)).source,_=await $(S.file,null),x=_?.items?.find(R=>R.id===g);o!==void 0&&(ii(x||u(`${t} has no item to set --next on`),o,await ni(e,t)),e&&await di(e,t,x,x.next));const O=D&&ht(x);return c&&x&&Rt(x,P)&&u(`builder done closes ${t}'s last open builder, which leaves it In Progress with no Next; pass --next "<what happens next>", or hand it back with item set ${t} --status checking or land`),await y(d,m),await mt(e,t),s&&await nn(e,t,n,f,{detail:s},w),(O||o!==void 0)&&await y(S.file,_),d}r(no,"stopBuilder");async function io(e){const t=[];for(const i of await q(e)){const n=await $(i.file,null);for(const a of n?.items||[])for(const s of await te(i.dir,a.id)){const o=await $(s,null);if(!o?.started||o.done)continue;const c=h.basename(s,".json");t.push({key:`${i.slug}/${a.id}`,agent:c.slice(String(a.id).length+1)||"main",label:o.label||"Builder",started:o.started})}}return t}r(io,"openBoardBuilders");async function Ac(e){const t=await io(e);for(const i of t)await no(e,i.key,{agent:i.agent,note:"Board closed. Builder record stopped; work is kept for reopening."});return t}r(Ac,"closeBoardBuilders");export{Q as BoardError,Pn as PUSH_LINE_MAX,Bt as UPLOAD_LIMIT,qo as WORKING_KINDS,ac as abandonLaunch,sc as abandonPlanner,er as ackPing,Wr as addItem,yr as addPrototype,vr as addQuestions,cr as addReviewers,mr as addRuleTo,kr as addTopic,$c as answerBuilder,re as appendThread,zr as archiveCards,Jr as archiveCount,kt as archiveFile,bt as archiveRank,nc as askImplement,gc as askOrchestrator,Dr as askedGate,uc as auditLog,Ur as backfillTickets,M as boardConfig,ee as boardRepoOf,Cr as breakOwnerCheck,xc as briefFacts,Mr as builderModel,wc as builderProgress,X as cardName,ec as cardPR,Zr as changeLineup,_c as checkCallsVerified,Pr as checkClose,Dc as checkNotReopened,Ac as closeBoardBuilders,Qr as createLineup,lc as cutOffBuilds,Hr as dropPlannedStatus,hr as editRuleIn,pc as ensureWorktree,Jo as eventCard,fr as fallBackFromLinear,tr as finishHandshake,Vr as finishLineup,Oc as finishesCard,jc as handBackCalls,qr as heldMerge,An as hiddenFile,Ar as isFinished,cc as isLanding,q as itemSources,vc as landable,mc as landingLine,ji as lineupNoteFile,kc as listAsks,yc as listCalls,br as listPrototypes,wr as listRules,Qo as markAsk,Uo as markRead,Fo as markWorking,Tr as mergeCard,fi as mergedLine,Kr as migrateFollowups,Zo as moveAsk,jr as moveQuestions,rc as openAgents,io as openBoardBuilders,ur as opensPRs,C as parseKey,Lr as passOwnerCheck,fc as pickBuilder,Er as pickUp,Xo as postIncomingReview,Br as postPR,Ir as prReport,ts as pushLine,as as pushUrl,yt as readArchive,_e as readAskLinks,ut as readAskProgress,Kt as readBoardRules,ms as readBoardSettings,Wn as readHandshake,os as readHidden,$ as readJSON,Yr as readLineupNotes,rs as readMarks,Nn as readMarksFile,Rr as readNotes,wt as readPrototypes,Ho as readUpload,Ga as readWorking,hc as recordCall,hi as recordOwnerTap,Za as recordPush,tc as recordRebase,Yo as recordReview,oc as recordSession,sn as recordedWorktree,zo as relateAsk,Sc as relaunchLander,xr as removePrototype,dr as removeReviewers,gr as removeRuleFrom,Qa as repliesSincePush,Or as resolveNoteAnswers,_r as resolveQuestion,ps as reviewDigest,rr as reviewDigestSent,sr as reviewDue,ar as reviewReact,ir as reviewSent,or as reviewUpdated,$r as saveBoardRules,Ue as saveBoardSettings,lr as saveBoardShare,Ko as saveUpload,Mo as sendPush,pr as setBoardSetting,Wo as setHidden,se as setItem,Nr as setMainInstall,nr as setReviewers,hs as settingsLines,ge as settingsOf,Sr as settleGates,Bo as siteDir,Ja as sitesDir,an as startBuilder,dc as startPlanner,Xr as startRebase,ic as startReleasedBuild,Js as startTappedBuild,no as stopBuilder,Gr as stripDeadFields,Gt as takeFile,Fr as tapMerge,U as threadFile,Zt as topicQuestions,Go as unfoldAsk,Pc as verifiedFinish,bc as verifyCalls,rt as wavesDir,es as withReplies,Vo as writeChecked,To as writeReply};
33
+ ${s}`}}r(gl,"readLineupNotes");async function q(e,t,n,a="main"){const{id:i}=R(t);/^[\w-]{1,40}$/.test(a)||d(`--agent takes a name of letters, digits, "_" and "-", not "${a}"`);const o=a==="main"?`${i}.json`:`${i}@${a}.json`;if(n)return g.join(g.resolve(n),"agents",o);const{source:s}=await x(e,t);return g.join(s.dir,"agents",o)}r(q,"builderFile");const vi=r((e,t)=>typeof e=="string"&&/^\d{4}-\d\d-\d\dT/.test(e)&&!Number.isNaN(Date.parse(e))?new Date(e).toISOString():d(`${t} takes an ISO time such as 2026-10-23T16:30:00-07:00`),"isoTime"),Ys=["reading","building","testing","checking","landing"],_i=80,Si="reading, building, testing, checking or landing";function ji(e,t,n,a){e||d(`${a} needs --site, since the line goes to the item's thread`),t!==void 0&&!Ys.includes(t)&&d(`--phase takes ${Si}, not "${t}"`);const i=j(n,a);return i.length>_i&&d(`${a} is ${i.length} characters; the cap is ${_i}. Say it shorter.`),{phase:t,detail:i}}r(ji,"progressLine");const Oi=r((e,t,n)=>t?{detail:e.detail}:(e.phase===void 0&&d(`${n} needs --phase ${Si}`),e),"forAgent");async function Fn(e,t,n="main",a,{phase:i,detail:o},s){const c=Fe(a),l=c||n==="main"?"":a.kind===ne?n:Ot(a)||a.kind||n,u=c?Ki[a.kind]:[l,i].filter(Boolean).join(" "),w=new RegExp(`^(?:${u}|${i??u})\\s*:?\\s+`,"i"),p=u?`${u[0].toUpperCase()}${u.slice(1)}: ${o.replace(w,"")}`:o,f=W(e,t);(await K(f)).filter(m=>m.kind==="status").pop()?.text!==p&&await Y(f,{ts:s,who:"claude",kind:"status",run:a.started,...i?{phase:i}:{},...c?{planner:!0}:{},text:p})}r(Fn,"statusToThread");const Xs={prototype:"sonnet",grill:"opus",[ue]:"sonnet"};async function mt(e,t,{label:n,phase:a,progress:i,dir:o,agent:s="main",kind:c,model:l,eta:u,worktree:w,ownWorktree:p,dispatch:f}){a!==void 0&&i===void 0&&d('--phase goes with --progress "<detail>"');const m=X(i,Q=>ji(e,a,Q,"--progress")),h=await q(e,t,o,s),k=new Date().toISOString(),v=X(c,Q=>/^[\w.-]{1,30}$/.test(Q)?Q:d("--kind takes a short word such as claude or codex")),E=nt(v),O=l??(v==="opus"||v==="sonnet"?v:void 0),B=["build",...rr].includes(E),S=await $(h,null),_=r(Q=>["tap","hub"].includes(Q?.dispatched)&&!Q.done,"tapOpen");if(f==="session"&&_(S))return{file:h,alreadyOpen:!0,lines:[]};s!=="main"&&v!==ue&&v!==ne&&_(await $(await q(e,t,o),null))&&d(`the Implement tap opened ${t}'s main builder record and no agent has taken it up; run builder start ${t} without --agent first`);const N=S?.dispatched&&!S.done?S:null,T=B&&!o?(await x(e,t)).item:void 0,C=S&&Ce(S).length?S:null,M=N&&E==="build"?{kind:nt(N.kind),model:N.model,reason:{tap:"The owner's Build tap picked it.",hub:"The board's pull picked it."}[N.dispatched]??"The session's dispatch picked it."}:B?cn({card:T??{}}):null;M&&E!=="build"&&E!==M.kind&&d(`--kind ${E}: ${M.kind} builds now. ${M.reason} Pass --kind ${M.kind}, or --kind build.`);const L=M?M.kind:E,ke=aa({dir:e});if(Yi(ke,L))throw new ae(Vi(ke,L));const me=m&&Oi(m,Fe({kind:L}),"--progress");l!==void 0&&!["opus","sonnet"].includes(l)&&d("--model takes opus or sonnet"),O!==void 0&&L==="codex"&&d("--model is for kind claude; codex needs none");const ie=L==="claude"?ln():"auto";O!==void 0&&ie!=="auto"&&O!==ie&&d(`--model ${O}: the hub's Claude model is ${ie==="opus"?"Opus":"Sonnet"}. Pass --model ${ie}, or no --model.`);const be=L==="claude"?O??M?.model??(N&&nt(N.kind)==="claude"?N.model:void 0)??"opus":Fe({kind:L})?O??(N?.kind===L?N.model:void 0)??Xs[L]:L===ne?O??S?.model:void 0,re={label:n===void 0?"Builder":j(n,"--label"),...L?{kind:L}:{},...be?{model:be}:{},started:k,...u===void 0?{}:{eta:vi(u,"--eta")},...f?{dispatched:f}:{},...C?{calls:C.calls,...C.verified?{verified:C.verified}:{}}:{},...ir(S)?{session:S.session}:{},...L===ne&&S?.kind===ne?{label:S.label,task:S.task,writes:S.writes,requested:S.requested}:{}},Ye=B&&!o&&!G(T)&&T.lane!=="progress",gt=Ye?await we(e,t,{start:!0}):{lines:[]};let ce,oe=null;const Zt=p&&B&&s==="main"?[S?.worktree,T?.worktree].find(Q=>Q?.path&&de(Q.path))??null:null,$t=(N??C)?.worktree??Zt,kt=w??(B&&s==="main"&&!$t?p:void 0);try{if(kt){const Q=await z(g.dirname(g.dirname(h)),R(t).id),Oe=(await Promise.all(Q.filter(le=>le!==h).map(le=>$(le,null)))).filter(le=>le?.worktree?.path&&!le.done).map(le=>le.worktree.path);({remade:oe,...ce}=await kt({busy:Oe}))}else ce=$t}catch(Q){throw Ye&&await sr(e,t,T),Q}await y(h,ce?{...re,worktree:ce}:re),T?.rebase_run&&s==="main"&&![Pi,qi].includes(re.label)&&await Ai(e,t),M?.inputs&&e&&wo(ia(e),{call:"builder-pick",key:t,result:[L,L==="claude"?be:null].filter(Boolean).join(" "),reason:M.reason,inputs:{...M.inputs,...O?{askedModel:O}:{},...f?{dispatch:f}:{}}}),oe&&e&&await U(e,t,{text:oe}),me&&await Fn(e,t,s,re,me,k);const ye=v===ue||v===ne?[]:await Ft(e,t,"Acted on: builder started");return e&&B&&await Ss(e,t),{file:h,kind:L??null,model:be??null,reason:M?.reason??null,worktree:ce?.path??null,lines:[...E==="build"?[`kind: ${L}. ${M.reason}`]:[],...Ot(re)?[`model: ${Ot(re)}`]:[],...e?[`ref: ${await J(e,t)}`]:[],...ce?[`worktree: ${ce.path}`]:[],...oe?[oe]:[],...gt.lines,...ye]}}r(mt,"startBuilder");async function er(e,t,n,a="tap"){try{const{held:i,blockers:o}=await nr(e,t);if(i&&await si(e,"card-hold",{key:t,hold:!1}),o.length)return await ar(e,t,o);try{const s=await mt(e,t,{kind:"build",worktree:n,dispatch:a});return{key:t,ref:await J(e,t),kind:s.kind,...s.model?{model:s.model}:{},reason:s.reason,worktree:s.worktree}}catch(s){throw i&&await or(e,t,i),s}}catch(i){if(!(i instanceof ae))throw i;return await Y(W(e,t),{ts:new Date().toISOString(),who:"claude",kind:ta,text:`Couldn't start implementing: ${i.message}`}),null}}r(er,"startTappedBuild");async function $l(e,t,{pr:n,conflict:a,base:i},o){const{source:s,wave:c,item:l}=await x(e,t);l.rebase_run={pr:n,base:i,conflict:a,at:new Date().toISOString()},await y(s.file,c);try{const u=await mt(e,t,{kind:"build",label:Pi,dispatch:"hub",ownWorktree:o});return{key:t,kind:u.kind,...u.model?{model:u.model}:{},worktree:u.worktree}}catch(u){throw await Ai(e,t),u}}r($l,"startRebase");const Pi="Rebase";async function Ai(e,t){const{source:n,wave:a,item:i}=await x(e,t);delete i.rebase_run,await y(n.file,a)}r(Ai,"endRebaseRun");async function kl(e,t){const{wave:n,item:a}=await x(e,t),i=te(e,n);return De(a,i).find(o=>o.repo===i)?.number??null}r(kl,"cardPR");async function bl(e,t,{from:n,to:a,replayed:i}){const{source:o,wave:s,item:c}=await x(e,t);return c.rebase_run?(c.rebased=[...c.rebased||[],{from:n,to:a,base:c.rebase_run.base,replayed:i,at:new Date().toISOString()}],delete c.rebase_run,await y(o.file,s),!0):!1}r(bl,"recordRebase");const tr=r((e,t,n)=>{const a=new Map((e.rebased||[]).filter(i=>i.replayed).map(i=>[i.to,i.from]));for(let i=n,o=0;i&&o<=a.size;i=a.get(i),o++)if(i===t)return!0;return!1},"onlyRebased");async function nr(e,t){const n=aa({dir:e});Xi(n)&&d(Zi(n));const{item:a}=await x(e,t),i=a.held?{held:a.held,pick_up_at:a.pick_up_at,held_at:a.held_at}:null;return i&&Qe(a)&&d("it's held. Unhold it, then tap Implement."),await In(e,t)&&d("no take is picked on its prototypes, so it prototypes again. Pick a take from the next round, then tap Implement."),{held:i,blockers:await Es(e,t,a)}}r(nr,"tapRefusals");async function yl(e,t,n,a){await U(e,t,{text:Pe.implement,who:"love",by:a,command:"implement"});const{item:i}=await x(e,t),o={state:i.state,unfiled:i.unfiled};await Di(e,t,{state:"ready",unfiled:void 0});const s=await er(e,t,n);return s||await Di(e,t,o),s}r(yl,"implementAsIs");async function Di(e,t,{state:n,unfiled:a}){const{source:i,wave:o,item:s}=await x(e,t);n===void 0?delete s.state:s.state=n,a===void 0?delete s.unfiled:s.unfiled=a,await y(i.file,o)}r(Di,"writeCardState");async function ar(e,t,n){const a=n.join(", ");return await se({dir:e},"card-bump",{key:t}),await Y(W(e,t),{ts:new Date().toISOString(),who:"claude",text:`Can\u2019t start yet: waits on ${a}. Bumped to the top of Todo.`}),{key:t,ref:await J(e,t),bumped:n,line:`bumped: ${t} waits on ${a}, so it stays in Todo, bumped to the top`}}r(ar,"bumpBlocked");async function xl(e,t,n){const a=await q(e,t),i=await $(a,null),o=new Date().toISOString();i&&!i.done&&await y(a,{...i,done:o});const{source:s,wave:c,item:l}=await x(e,t);l.lane==="progress"&&Ln(l,"todo",o),await y(s.file,c),await Y(W(e,t),{ts:o,who:"claude",kind:ta,text:`Couldn't start implementing: ${n}`})}r(xl,"abandonLaunch");async function vl(e,t,n,a="main"){const i=await q(e,t,void 0,a),o=await $(i,null);o&&!o.done&&await y(i,{...o,done:new Date().toISOString()}),await Bt(e,t),await U(e,t,{text:n})}r(vl,"abandonPlanner");async function _l(e,t,n,a,i="main",o=null){const s=await q(e,t,void 0,i),c=await $(s,null);await y(s,{...c,session:{id:n,pid:a,log:o,at:new Date().toISOString()}})}r(_l,"recordSession");async function Sl(e,t){const{source:n}=await x(e,t);return(await Promise.all((await z(n.dir,R(t).id)).map(i=>$(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({pid:i.session?.pid??null,at:i.session?.at??null}))}r(Sl,"openAgents");async function jl(e,t){const{source:n}=await x(e,t);return(await Promise.all((await z(n.dir,R(t).id)).map(i=>$(i,null)))).filter(i=>i&&i.started&&!i.done).map(i=>({kind:i.kind??null,pid:i.session?.pid??null,at:i.session?.at??null}))}r(jl,"openRuns");async function Ol(e,t,n){const{source:a}=await x(e,t),i=new Date().toISOString();for(const o of await z(a.dir,R(t).id)){const s=await $(o,null);s?.started&&!s.done&&tt(s)&&await y(o,{...s,stopped:{run:n,at:i}})}}r(Ol,"markStopped");async function Pl(e,t){const{source:n,wave:a,item:i}=await x(e,t);i.merge_ready&&(i.merge_ready.stopped=new Date().toISOString(),await y(n.file,a))}r(Pl,"stopHeldMerge");async function Al(e,t,n){const a=j(n,"item stall --reason"),{source:i,wave:o,item:s}=await x(e,t);return s.lane==="done"&&d(`${await J(e,t)} is done; there is nothing to stall`),s.stalled={reason:a,at:new Date().toISOString()},await y(i.file,o),a}r(Al,"markStalled");async function Dl(e,t){const{source:n,wave:a,item:i}=await x(e,t);i.stalled&&(delete i.stalled,await y(n.file,a))}r(Dl,"clearStall");async function Rl(e,t){const{source:n}=await x(e,t),a=await K(g.join(e,"data","events.jsonl")),i=r(l=>a.filter(u=>u.kind===l&&u.data?.key===t).map(u=>String(u.ts)).sort().pop()||"","latest"),o=i("card-bump");if(!o||i("card-unbump")>o)return null;const c=(await Promise.all((await z(n.dir,R(t).id)).map(l=>$(l,null)))).filter(l=>l&&tt(l)).map(l=>String(l.started||"")).filter(Boolean).sort()[0]||"";return c&&o<c?null:o}r(Rl,"landFirst");async function Nl(e,t,n){const{item:a}=await x(e,t);G(a)&&d(`${await J(e,t)} is done; there is nothing to bump`);const i=await se({dir:e},n?"card-bump":"card-unbump",{key:t});return(await K(g.join(e,"data","events.jsonl"))).find(o=>o.id===i).ts}r(Nl,"bumpCard");async function El(e,t){return(await K(W(e,t))).filter(n=>n.kind==="status").pop()?.phase==="landing"}r(El,"isLanding");const ir=r(e=>!!e?.session&&e.session.at>(e.done??e.started),"launchedSession");async function Il(e,t,n){const a=await $(await q(e,t),null);if(a&&!a.done)return null;const i=await mt(e,t,{kind:n,dispatch:"hub"});return{key:t,kind:n,model:i.model}}r(Il,"startPlanner");async function Ll(e,t,n){const{source:a}=await x(e,t);if((await Promise.all((await z(a.dir,R(t).id)).map(c=>$(c,null)))).some(c=>c?.kind===ue&&c.started&&!c.done))return null;const o=Qi(n);if(await $(await q(e,t,void 0,o),null))return null;const s=await mt(e,t,{kind:ue,agent:o,dispatch:"hub"});return{key:t,kind:ue,model:s.model,agent:o}}r(Ll,"startVisual");const Mn=r(e=>typeof e=="string"&&/^[a-z][a-z0-9-]{0,29}$/.test(e)&&e!=="main"&&!e.startsWith(`${ue}-`)?e:d('--name takes a short lowercase name such as tests or docs-pass: letters, digits and "-", not main or visual-\u2026'),"helperName");async function Wn(e,t){const{item:n}=await x(e,t);return[(await $(await q(e,t),null))?.worktree,n.worktree].find(i=>i?.path&&de(i.path))??null}r(Wn,"cardWorktree");async function Bl(e,t,{name:n,task:a,writes:i,model:o}){A(e);const s=Mn(n),c=j(a,"--task");o!==void 0&&!["opus","sonnet"].includes(o)&&d("--model takes opus or sonnet");const l=ln();o!==void 0&&l!=="auto"&&o!==l&&d(`--model ${o}: the hub's Claude model is ${l==="opus"?"Opus":"Sonnet"}. Pass --model ${l}, or no --model.`);const{item:u}=await x(e,t);G(u)&&d(`${await J(e,t)} is Done; a helper needs an open card`);const w=await q(e,t,void 0,s);return de(w)&&d(`${t} already has an agent named ${s}; pick another --name`),i&&!await Wn(e,t)&&d(`a helper that writes branches from ${t}'s worktree, and the card has none; start its build first, or drop --writes`),await y(w,{label:s,kind:ne,task:c,writes:!!i,...o?{model:o}:{},requested:new Date().toISOString()}),await U(e,t,{text:`Asked for helper ${s}${i?", which writes code":""}: ${c}`}),{lines:[`helper ${s} requested: the board launches it ${i?"once an implementer slot is free":"now"}`,`wait for it with agent wait ${t} --name ${s}`]}}r(Bl,"requestHelper");async function Cl(e,t,n,a){const i=await q(e,t,void 0,n),o=await $(i,null);if(o?.kind!==ne||o.started||o.done)return null;const{item:s}=await x(e,t),c=cn({card:s}),l=ln(),u=o.model??(l!=="auto"?l:c.kind==="claude"?c.model:"opus"),w=o.writes?await Wn(e,t):null;o.writes&&!w&&d(`${t} has no worktree for helper ${n} to branch from`),await y(i,{...o,model:u});const p=await mt(e,t,{agent:n,kind:ne,dispatch:"hub",worktree:w?({busy:f})=>a({repo:w.repo,base:w.branch,busy:f}):void 0});return{key:t,kind:ne,model:u,agent:n,task:o.task,writes:o.writes,worktree:p.worktree}}r(Cl,"startHelper");async function Tl(e,t,n){const a=await $(await q(e,t,void 0,Mn(n)),null);return a?.kind!==ne&&d(`${t} has no helper named ${n}`),{record:a,card:await Wn(e,t)}}r(Tl,"helperOf");async function ql(e,t,{name:n,agent:a="main",timeout:i=9,every:o=2e3,alive:s}){A(e);const c=Number(i);c>0||d("--timeout takes minutes, a number above 0");const l=await q(e,t,void 0,Mn(n)),u=await q(e,t,void 0,a),w=r(async f=>{const m=await $(u,null);if(!m?.started||m.done)return;const{waits:h,...k}=m;await y(u,f?{...k,waits:f}:k)},"mark"),p=Date.now()+c*6e4;await w(n);try{for(;;){const f=await $(l,null);if(f?.kind!==ne&&d(`${t} has no helper named ${n}`),f.done){const h=f.writes&&f.worktree?[`branch ${f.worktree.branch} in ${f.worktree.path}: merge it with agent merge ${t} --name ${n}`]:[];return{lines:[`helper ${n} done: ${f.note??"no hand-back"}`,...h]}}const m=f.session?.pid??null;m&&!s(m)&&d(`helper ${n}'s process exited without builder done; do its task yourself`),Date.now()>=p&&d(`helper ${n} is still ${f.started?"running":f.writes?"waiting for an implementer slot":"waiting to launch"} after ${c} minutes; run agent wait again`),await new Promise(h=>setTimeout(h,o))}}finally{await w(null)}}r(ql,"waitHelper");async function Fl(e,t,n="main"){return n==="main"?!1:(await $(await q(e,t,void 0,n),null))?.kind===ne}r(Fl,"isHelper");async function Ml(e,t,{file:n,title:a}){A(e);const i=j(a,"--title");(typeof n!="string"||!n.endsWith(".html"))&&d("--file takes the page, an .html file");const o=await P.readFile(g.resolve(n),"utf8").catch(()=>d(`--file ${n} can't be read`));o.trim()||d(`--file ${n} is empty`),await x(e,t);const{id:s}=R(t),c=g.join(e,"public","visuals",s);await P.mkdir(c,{recursive:!0});const l=i.toLowerCase().replace(/[^a-z0-9]+/g,"-").slice(0,60).replace(/^-+|-+$/g,"")||"visual";let u=`${l}.html`;for(let p=2;de(g.join(c,u));p++)u=`${l}-${p}.html`;await P.writeFile(g.join(c,u),o,{flag:"wx"});const w=`visuals/${s}/${u}`;return await U(e,t,{text:i,link:w,kind:ue}),{lines:[w]}}r(Ml,"addVisual");async function Wl(e){const t=[];for(const n of await F(e)){const a=await $(n.file,null);for(const i of a?.items||[]){if(G(i))continue;const o=await $(g.join(n.dir,"agents",`${i.id}.json`),null);!o?.started||o.done||!o.dispatched||Fe(o)||t.push({key:`${n.slug}/${i.id}`,kind:nt(o.kind),model:o.model})}}return t}r(Wl,"cutOffBuilds");async function or(e,t,n){const{source:a,wave:i,item:o}=await x(e,t);for(const s of Object.keys(n))n[s]===void 0?delete o[s]:o[s]=n[s];await y(a.file,i)}r(or,"restoreHold");async function sr(e,t,n){const{source:a,wave:i,item:o}=await x(e,t);for(const s of["lane","lane_at","closed_at","reopened_at"])n[s]===void 0?delete o[s]:o[s]=n[s];await y(a.file,i)}r(sr,"restoreLane");async function Ul(e,t){return e||d("audit reads a board: pass --site <route>"),t!==void 0&&R(t),{lines:mo(ho(ia(e),{key:t}))}}r(Ul,"auditLog");const rr=["codex","claude"];async function Hl(e,t){const{item:n}=await x(e,t),{kind:a,model:i,reason:o}=cn({card:n});return{lines:[`${[a,i].filter(Boolean).join(" ")}: ${o}`]}}r(Hl,"pickBuilder");async function Ri(e,t,{dir:n}={}){const{id:a}=R(t),i=g.dirname(await q(e,t,n)),o=await z(g.dirname(i),a);return(await Promise.all(o.map(c=>$(c,null)))).filter(c=>c?.worktree?.path).sort((c,l)=>String(l.started).localeCompare(String(c.started)))[0]?.worktree??null}r(Ri,"recordedWorktree");async function zl(e,t,n,{phase:a,dir:i,agent:o="main",eta:s,clearEta:c}){s!==void 0&&c&&d("pass --eta or --clear-eta, not both");const l=ji(e,a,n,"builder progress"),u=await q(e,t,i,o),w=await $(u,null);(!w?.started||w.done)&&d(`builder progress needs an open builder record on ${t}; run builder start first`);const p=Oi(l,Fe(w),"builder progress");return s!==void 0&&(w.eta=vi(s,"--eta")),c&&delete w.eta,(s!==void 0||c)&&await y(u,w),await Fn(e,t,o,w,p,new Date().toISOString()),u}r(zl,"builderProgress");async function Kl(e,t){await x(e,t);const n=W(e,t);(await K(n)).filter(a=>a.kind==="status").pop()?.phase!=="landing"&&await Y(n,{ts:new Date().toISOString(),who:"claude",kind:"status",phase:"landing",text:"Landing: rebasing onto main and landing"})}r(Kl,"landingLine");const Ce=r(e=>(e?.calls||[]).filter(t=>!e.verified||t.at>e.verified.at),"unverifiedCalls");async function Jl(e,t,n,{dir:a,agent:i="main"}){const o=j(n,"builder call");/\n/.test(o)&&d("builder call takes one line; record each call on its own");const s=await q(e,t,a,i),c=await $(s,null);return(!c?.started||c.done)&&d(`builder call needs an open builder record on ${t}; run builder start first`),await y(s,{...c,calls:[...c.calls||[],{at:new Date().toISOString(),text:o}]}),s}r(Jl,"recordCall");async function ht(e,t){const n=await K(W(e,t)),a=new Map(n.filter(i=>i.kind===St.answer).map(i=>[i.ask,i]));return n.filter(i=>i.kind===St.ask).map(i=>{const o=a.get(i.ask);return{id:i.ask,agent:i.agent||"main",text:i.text,at:i.ts,answer:o?{text:o.text,at:o.ts}:null,deferred:!!o?.deferred}})}r(ht,"builderAsks");const cr=r(e=>fe().find(t=>he(t.dir,e))??{dir:e},"boardTarget");async function Gl(e,t,n,{agent:a="main"}){A(e);const i=j(n,"builder ask");/\n/.test(i)&&d("builder ask takes one line; ask each question on its own");const o=await $(await q(e,t,void 0,a),null);(!o?.started||o.done)&&d(`builder ask needs an open builder record on ${t}; run builder start first`);const s=`ask${(await ht(e,t)).length+1}`;return await Y(W(e,t),{ts:new Date().toISOString(),who:`${Ot(o)||o.kind||"Claude"} builder`,kind:St.ask,ask:s,agent:a,text:i}),await se(cr(e),"builder-ask",{key:t,ask:s,agent:a,text:i}),{id:s,lines:[]}}r(Gl,"askOrchestrator");async function Ql(e,t,n,a,{deferred:i=!1}={}){A(e);const o=j(a,"--text"),s=(await ht(e,t)).find(l=>l.id===n)||d(`no ask "${n}" on ${t}; builder asks ${t} lists them`);s.answer&&d(`${n} on ${t} is already ${s.deferred?"deferred":"answered"}`);const c=new Date().toISOString();if(await Y(W(e,t),{ts:c,who:"claude",kind:St.answer,ask:n,...i?{deferred:!0}:{},text:o}),i){const l=await q(e,t,void 0,s.agent),u=await $(l,null);u&&await y(l,{...u,calls:[...u.calls||[],{at:c,text:`Question: ${s.text} (deferred to the owner)`}]})}return{lines:[]}}r(Ql,"answerBuilder");async function Zl(e,t){A(e);const n=await ht(e,t);return n.length?{lines:n.flatMap(a=>[`${a.id}. ${a.text}`,` ${a.answer?`${a.deferred?"deferred":"answered"}: ${a.answer.text}`:"waiting on the orchestrator"}`])}:{lines:["no asks yet"]}}r(Zl,"listAsks");async function lr(e,t,n){const a=t.topic.startsWith("item:")&&!t.topic.startsWith("item:ask:")?t.topic.slice(5):d(`${n}.ask goes on a card's question, topic item:<wave>/<id>`),i=(await ht(e,a)).find(o=>o.id===t.ask)||d(`${n}.ask names no ask "${t.ask}" on ${a}; builder asks ${a} lists them`);i.deferred||d(`${n}.ask names ${i.id}, which the orchestrator ${i.answer?"answered":"has not deferred"}; only a deferred ask goes to the owner`)}r(lr,"checkAskQuestion");async function Vl(e,t,{dir:n,agent:a="main"}){const i=await $(await q(e,t,n,a),null),o=i?.calls||[];if(!o.length)return{lines:["no judgment calls recorded"]};const{verified:s}=i,c=Ce(i).length?"not verified yet":`verified ${s.at}${s.note?`: ${s.note}`:""}`;return{lines:[...o.map((l,u)=>`${u+1}. ${l.text}`),c]}}r(Vl,"listCalls");async function Yl(e,t,{dir:n,agent:a="main",note:i}){const o=await q(e,t,n,a),s=await $(o,null);if(Ce(s).length||d(`${t} has no unverified judgment calls${a==="main"?"":` from agent ${a}`}; builder calls ${t} lists them`),e){const c=new Set((await $(I(e),{items:[]})).items?.filter(u=>u.topic===`item:${t}`&&u.ask).map(u=>u.ask)),l=(await ht(e,t)).find(u=>u.deferred&&u.agent===a&&!c.has(u.id));l&&d(`${l.id} on ${t} was deferred to the owner; add its question with "ask": "${l.id}" first`)}return await y(o,{...s,verified:{at:new Date().toISOString(),...i===void 0?{}:{note:j(i,"--note")}}}),o}r(Yl,"verifyCalls");async function Xl(e,t){const{source:n,item:a}=await x(e,t),i=r((s,c)=>[(s.options||[]).find(l=>l.id===c.choice)?.label,c.note].filter(Boolean).join(": ")||"answered","said"),o=(await pe(e,t)).asked.filter(s=>s.ask&&s.answers.length).map(s=>({ask:s.ask,question:s.question,answer:i(s,s.answers.at(-1))}));return{key:t,ref:await J(e,t),title:a.title||t,notes:a.plan?g.join(n.dir,a.plan):null,worktree:(await Ri(e,t))?.path??null,answers:o,links:await dr(e,t),rebasePR:a.rebase_run?.pr??null}}r(Xl,"briefFacts");async function dr(e,t){const n=new Map;for(const c of await F(e))for(const l of(await $(c.file,null))?.items||[])n.set(`${c.slug}/${l.id}`,{entry:c,it:l});const a=R(t).wave,{it:i}=n.get(t),o=[...n].filter(([,c])=>(c.it.related||[]).some(l=>H(c.entry.slug,l)===t)).map(([c])=>c),s=[...i.parent?[["parent",H(a,i.parent)]]:[],...(i.deps||[]).map(c=>["depends on",H(a,c)]),...[...new Set([...(i.related||[]).map(c=>H(a,c)),...o])].map(c=>["related",c])].filter(([,c])=>n.has(c));return Promise.all(s.map(async([c,l])=>{const{entry:u,it:w}=n.get(l);return{relation:c,ref:await J(e,l),title:w.title||l,lane:jt[w.lane||"todo"],notes:w.plan?g.join(u.dir,w.plan):null}}))}r(dr,"cardLinks");async function ed(e){const t=[];for(const n of await F(e)){const a=await $(n.file,null);for(const i of a?.items||[]){if(G(i))continue;const o=(await Promise.all((await z(n.dir,i.id)).map(u=>$(u,null)))).filter(Boolean);if(!nn(o))continue;const s=o.map(u=>u.verified?.at).filter(Boolean).sort().at(-1);if(!s||o.some(u=>Ce(u).length))continue;const c=`${n.slug}/${i.id}`;if((await pe(e,c)).asked.some(u=>u.state==="open")||await Mt(e,c,i))continue;const l=o.filter(u=>u.worktree?.path).sort((u,w)=>String(w.started).localeCompare(String(u.started)))[0];l&&de(l.worktree.path)&&t.push({key:c,verified:s,kind:nt(l.kind),model:l.model})}}return t}r(ed,"landable");async function td(e,t){const{source:n,item:a}=await x(e,t);(await Promise.all((await z(n.dir,a.id)).map(c=>$(c,null)))).some(c=>c?.started&&!c.done)||d(`${t} has no open builder; only a card whose lander stopped has one to relaunch`),await we(e,t,{handBack:!0,next:"A lander relaunches to land the verified build"});const o=g.join(e,"data","landers.json"),s=await $(o,{});delete s[t],await P.writeFile(o+".tmp",JSON.stringify(s)+`
34
+ `),await P.rename(o+".tmp",o)}r(td,"relaunchLander");async function nd(e,t,{dir:n}={}){const{id:a}=R(t),i=await z(g.dirname(g.dirname(await q(e,t,n))),a),o=[];for(const s of i){if(!Ce(await $(s,null)).length)continue;const c=g.basename(s,".json").slice(String(a).length+1)||"main";o.push(c==="main"?"":` --agent ${c}`)}o.length&&d(`land: ${t} has judgment calls nobody verified yet; check them with ${o.map(s=>`builder calls ${t}${s}`).join(", ")}, then run ${o.map(s=>`builder verified ${t}${s}`).join(", ")}`)}r(nd,"checkCallsVerified");async function ad(e,t,{dir:n,agent:a="main",next:i}){const o=await $(await q(e,t,n,a),null),s=o&&!o.done?Ce(o):[];if(!s.length)return null;i!==void 0&&d(`builder done sets ${t}'s Next itself while its record has judgment calls; run it without --next`);const c=s.some(u=>u.text.startsWith("Question:"))?"Answer the builder's questions":"Verify the builder's judgment calls",l=await we(e,t,{handBack:!0,next:c});return{lines:[`handed back: Next "${c}"`,...l.lines]}}r(ad,"handBackCalls");async function id(e,t,{agent:n="main"}){if(n!=="main")return!1;const a=await q(e,t,void 0,n),i=await $(a,null);if(!i?.started||i.done||!i.worktree?.path)return!1;const{id:o}=R(t);for(const s of await z(g.dirname(g.dirname(a)),o)){const c=s===a?null:await $(s,null);if(!c?.started||c.done||c.kind==="install")continue;const l=g.basename(s,".json").slice(String(o).length+1);d(`builder done would finish ${t} while agent ${l} is still open; run builder done ${t} --agent ${l} first`)}return!0}r(id,"finishesCard");async function od(e,t){const{source:n,item:a}=await x(e,t);if(G(a))return null;const i=(await Promise.all((await z(n.dir,a.id)).map(s=>$(s,null)))).filter(Boolean);if(!nn(i)||i.some(s=>Ce(s).length||s.started&&!s.done&&s.kind!=="install"))return null;const o=await Ri(e,t);return!o||!de(o.path)?null:{waits:(await pe(e,t)).asked.filter(s=>s.state==="open").map(s=>s.id)}}r(od,"verifiedFinish");async function sd(e,t){const{item:n}=await x(e,t);if(n.reopened_at&&d(`the owner reopened ${await J(e,t)}; it closes on the owner's Done`),n.lane!=="done"||st(n).some(o=>o.merged_at))return;const a=String(n.closed_at||"");(await K(g.join(e,"data","events.jsonl"))).some(o=>o.kind==="item-message"&&o.data?.key===t&&String(o.ts)>a)&&d(`the owner reopened ${await J(e,t)} with a message; it closes on the owner's Done`)}r(sd,"checkNotReopened");async function z(e,t){const n=g.join(e,"agents");return(await P.readdir(n).catch(()=>[])).filter(a=>a===`${t}.json`||a.startsWith(`${t}@`)&&a.endsWith(".json")).map(a=>g.join(n,a))}r(z,"builderFiles");async function Un(e,t,n,{handedBack:a,endHandBack:i=!1,held:o=!1}={}){const s=new Date().toISOString();for(const c of await z(e,t)){const l=await $(c,null);if(!l)continue;if(l.done||l.kind==="install"&&!n){if(i&&l.handed_back){const{handed_back:f,...m}=l;await y(c,m)}continue}const{eta:u,handed_back:w,...p}=l;await y(c,{...p,done:s,...a?{handed_back:a}:{},...o&&l.started?{hold_stopped:!0}:{}})}}r(Un,"closeBuilders");async function rd(e,t){const n=await q(e,t),a=await $(n,null);if(!a?.hold_stopped)return null;const{hold_stopped:i,...o}=a;return await y(n,o),{kind:a.kind??null,label:a.label??null}}r(rd,"takeHoldStopped");async function ur(e,t,{dir:n,agent:a,startedBetween:i,note:o,next:s,requireNext:c=!1}){const l=await q(e,t,n,a),u=await $(l,null);if(!u||u.done)return l;if(i){const _=Date.parse(u.started);if(!(_>=i[0]&&_<=i[1]))return l}const{eta:w,...p}=u,f=new Date().toISOString(),m={...p,done:f,...o?{note:o}:{}},{id:h}=R(t),k=g.dirname(g.dirname(l)),v=await Promise.all((await z(k,h)).map(_=>_===l?m:$(_,null))),E=!v.some(_=>_?.started&&!_.done),O=n?{file:g.join(k,"wave.json")}:(await x(e,t)).source,B=await $(O.file,null),S=B?.items?.find(_=>_.id===h);return s!==void 0&&(Qa(S||d(`${t} has no item to set --next on`),s,await Ga(e,t)),e&&await Xa(e,t,S,S.next)),c&&S&&on(S,v)&&d(`builder done closes ${t}'s last open builder, which leaves it In Progress with no Next; pass --next "<what happens next>", or record a judgment call with builder call ${t} or land`),await y(l,m),await Bt(e,t),o&&await Fn(e,t,a,u,{detail:o},f),s!==void 0&&await y(O.file,B),l}r(ur,"stopBuilder");async function fr(e){const t=[];for(const n of await F(e)){const a=await $(n.file,null);for(const i of a?.items||[])for(const o of await z(n.dir,i.id)){const s=await $(o,null);if(!s?.started||s.done)continue;const c=g.basename(o,".json");t.push({key:`${n.slug}/${i.id}`,agent:c.slice(String(i.id).length+1)||"main",label:s.label||"Builder",started:s.started})}}return t}r(fr,"openBoardBuilders");async function cd(e){const t=await fr(e);for(const n of t)await ur(e,n.key,{agent:n.agent,note:"Board closed. Builder record stopped; work is kept for reopening."});return t}r(cd,"closeBoardBuilders");export{ae as BoardError,pa as PUSH_LINE_MAX,gn as UPLOAD_LIMIT,Kr as WORKING_KINDS,xl as abandonLaunch,vl as abandonPlanner,rc as ackPing,sl as addItem,Cc as addNotes,Rc as addPrototype,Lc as addQuestions,$c as addReviewers,jc as addRuleTo,Dc as addTopic,Ml as addVisual,Ql as answerBuilder,U as appendThread,fl as archiveCards,ul as archiveCount,zt as archiveFile,Jt as archiveRank,Gl as askOrchestrator,Jc as askedGate,Ul as auditLog,rl as backfillTickets,Mt as blockWords,ee as boardConfig,te as boardRepoOf,Xc as breakOwnerCheck,Xl as briefFacts,ol as builderModel,zl as builderProgress,Nl as bumpCard,hc as cardLinked,J as cardName,kl as cardPR,ml as changeLineup,nd as checkCallsVerified,Kc as checkClose,sd as checkNotReopened,Dl as clearStall,cd as closeBoardBuilders,wl as createLineup,Wl as cutOffBuilds,Oc as editRuleIn,Xr as eventCard,vc as fallBackFromLinear,cc as finishHandshake,hl as finishLineup,id as finishesCard,ad as handBackCalls,al as heldMerge,Tl as helperOf,ma as hiddenFile,yl as implementAsIs,ec as intakeHandoff,Qc as isFinished,Fl as isHelper,El as isLanding,F as itemSources,Rl as landFirst,ed as landable,Kl as landingLine,$i as lineupNoteFile,Zl as listAsks,Vl as listCalls,Nc as listPrototypes,Sc as listRules,ac as markAsk,Zr as markRead,Al as markStalled,Ol as markStopped,Jr as markWorking,nl as mergeCard,ni as mergedLine,cl as migrateFollowups,ll as migrateStatuses,ic as moveAsk,Gc as moveCard,Uc as moveQuestions,Sl as openAgents,fr as openBoardBuilders,jl as openRuns,xc as opensPRs,R as parseKey,Yc as passOwnerCheck,Hl as pickBuilder,Vc as pickUp,Mc as planAgain,gc as postDigest,sc as postIncomingReview,dc as postLink,tl as postPR,uc as postReply,el as prReport,Jo as pushLine,Zo as pushUrl,pc as queuePushes,Ds as quickAsk,Ve as readArchive,Ie as readAskLinks,Nt as readAskProgress,vn as readBoardRules,ss as readBoardSettings,Sa as readHandshake,Yo as readHidden,$ as readJSON,gl as readLineupNotes,Xo as readMarks,ga as readMarksFile,Ts as readNotes,It as readPRSlack,ft as readPrototypes,Yr as readUpload,Fo as readWorking,Jl as recordCall,si as recordOwnerTap,Wo as recordPush,fc as recordReaction,bl as recordRebase,oc as recordReview,_l as recordSession,Ri as recordedWorktree,tc as relateAsk,td as relaunchLander,Ic as removePrototype,kc as removeReviewers,Pc as removeRuleFrom,Mo as repliesSincePush,Bl as requestHelper,Hc as resolveNoteAnswers,Wc as resolveQuestion,is as reviewDigest,wc as reviewDue,mc as reviewUpdated,Ac as saveBoardRules,Ke as saveBoardSettings,bc as saveBoardShare,Vr as saveUpload,Gr as sendPush,Ec as servedRoundsOpen,yc as setBoardPause,_c as setBoardSetting,Qr as setHidden,we as setItem,Zc as setMainInstall,lc as setReviewers,rs as settingsLines,$e as settingsOf,Bc as settleAnswers,Hr as siteDir,Co as sitesDir,Tc as splitSharedNotes,qc as stageImplement,dl as stageImplementAsks,Fc as stageReply,mt as startBuilder,Cl as startHelper,Il as startPlanner,$l as startRebase,er as startTappedBuild,Ll as startVisual,ur as stopBuilder,Pl as stopHeldMerge,pl as stripDeadFields,On as takeFile,rd as takeHoldStopped,il as tapMerge,W as threadFile,Ka as topicQuestions,nc as unfoldAsk,zc as unrecorded,od as verifiedFinish,Yl as verifyCalls,ql as waitHelper,At as wavesDir,Ko as withReplies,zr as writeReply};