@lovek4d/kickflip 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CONTEXT.md +32 -16
  2. package/SKILL.md +8 -6
  3. package/bin/doctor.mjs +4 -4
  4. package/bin/install.mjs +3 -3
  5. package/bin/kickflip.mjs +8 -8
  6. package/bin/wait-events.mjs +2 -2
  7. package/board/bin/board.mjs +50 -28
  8. package/board/domain.mjs +7 -7
  9. package/board/finish.mjs +4 -3
  10. package/board/headless.mjs +4 -3
  11. package/board/merge3.mjs +1 -0
  12. package/board/on-host.mjs +1 -1
  13. package/board/permissions.mjs +2 -2
  14. package/board/public/app-asks.js +6 -89
  15. package/board/public/app-cards.js +19 -10
  16. package/board/public/app-core.js +1 -20
  17. package/board/public/app-reader.js +174 -0
  18. package/board/public/app-threads.js +49 -35
  19. package/board/public/app.html +2 -0
  20. package/board/public/app.js +28 -16
  21. package/board/public/hub.css +1 -0
  22. package/board/public/new-task.js +121 -0
  23. package/board/public/shell.js +4 -0
  24. package/board/public/sidebar.css +1 -0
  25. package/board/public/style.css +91 -3
  26. package/board/public/ui-cards.js +46 -21
  27. package/board/public/ui-enums.js +1 -1
  28. package/board/public/ui-phase-bars.js +4 -4
  29. package/board/public/ui-questions.js +47 -18
  30. package/board/public/ui-sidebar.js +9 -3
  31. package/board/public/ui-threads.js +25 -12
  32. package/board/public/ui.js +42 -12
  33. package/board/public/view-frame.js +1 -0
  34. package/board/public/view-questions.js +45 -16
  35. package/board/public/view-settings.js +38 -19
  36. package/board/server.mjs +20 -18
  37. package/board/store.mjs +15 -13
  38. package/board/worktree.mjs +20 -20
  39. package/hub/boards.mjs +4 -4
  40. package/hub/front-door.mjs +20 -0
  41. package/hub/github.mjs +3 -3
  42. package/hub/hub.mjs +9 -12
  43. package/hub/new-board.mjs +2 -2
  44. package/hub/owner-checks.mjs +1 -0
  45. package/hub/readiness.mjs +2 -2
  46. package/hub/sessions.mjs +2 -2
  47. package/hub/try.mjs +7 -7
  48. package/package.json +1 -1
  49. package/shared/owner-check.mjs +3 -0
  50. package/shared/tailscale.mjs +1 -0
  51. package/shared/usage-gate.mjs +1 -1
@@ -1,21 +1,21 @@
1
- var D=Object.defineProperty;var c=(r,e)=>D(r,"name",{value:e,configurable:!0});import p from"node:fs";import k from"node:path";import E from"node:os";import{randomUUID as F}from"node:crypto";import{spawnSync as T}from"node:child_process";import{fileURLToPath as N}from"node:url";import{BoardError as A}from"./store.mjs";import{boardAt as O,hubCode as M,listBoards as P,readManifest as _}from"../hub/boards.mjs";import{boardRepo as q}from"../hub/board-repo.mjs";import{cloneRoot as B}from"../shared/repos.mjs";import{storedPath as j}from"../bin/install.mjs";import{hubService as K}from"../shared/service.mjs";const L=k.join(k.dirname(N(import.meta.url)),"..","bin","kickflip.mjs"),W=B,J=c(()=>k.join(E.homedir(),".claude","kickflip","worktrees"),"worktreesDir"),f=c(r=>{throw new A(r)},"fail");function a(r,...e){const t=T("git",["-C",r,"-c","core.commentChar=;",...e],{encoding:"utf8"});return{ok:t.status===0,out:(t.stdout||"").trim(),err:(t.stderr||t.error?.message||"").trim().split(`
2
- `)[0]}}c(a,"git");const I=c((r,e)=>a(r,"rev-parse","--short",e).out,"short");function H(r,{repo:e=W(),base:t="main",remote:n=!1,busy:o=[]}={}){const s=k.resolve(e);a(s,"rev-parse","--git-dir").ok||f(`--repo ${s} is not a git repo`),a(s,"check-ref-format","--branch",r).ok||f(`--worktree: "${r}" can't be a branch name`),a(s,"check-ref-format","--branch",t).ok||f("--worktree: invalid base branch");const l=n?`refs/remotes/origin/${t}`:`refs/heads/${t}`;n&&a(s,"fetch","--quiet","--no-tags","origin",`+refs/heads/${t}:${l}`),a(s,"rev-parse","--verify","--quiet",l).ok||f(`--worktree: ${s} has no ${t} branch`);const u=k.join(J(),r),g=p.existsSync(u)?U(s,u,r,{ref:l,base:t,busy:o}):null;a(s,"rev-parse","--verify","--quiet",`refs/heads/${r}`).ok&&f(`--worktree: branch ${r} already exists in ${s}`),p.mkdirSync(k.dirname(u),{recursive:!0});const w=a(s,"worktree","add","--quiet","-b",r,u,l);return w.ok||f(`--worktree: git worktree add failed: ${w.err}`),{path:j(u),branch:r,repo:j(s),remade:g}}c(H,"makeWorktree");function U(r,e,t,{ref:n,base:o,busy:s}){const l=c(i=>f(`--worktree: ${e} ${i}; land or remove it first`),"refuse");s.includes(j(e))&&l("already exists and another open builder record uses it");const u=p.realpathSync(e);a(r,"worktree","list","--porcelain").out.split(`
1
+ var P=Object.defineProperty;var a=(e,t)=>P(e,"name",{value:t,configurable:!0});import h from"node:fs";import w from"node:path";import E from"node:os";import{randomUUID as B}from"node:crypto";import{spawnSync as T}from"node:child_process";import{fileURLToPath as L}from"node:url";import{BoardError as M}from"./store.mjs";import{boardAt as _,hubCode as K,listBoards as J,readManifest as H}from"../hub/boards.mjs";import{boardRepo as U}from"../hub/board-repo.mjs";import{cloneRoot as Y}from"../shared/repos.mjs";import{storedPath as x}from"../bin/install.mjs";import{hubService as X}from"../shared/service.mjs";const Q=w.join(w.dirname(L(import.meta.url)),"..","bin","kickflip.mjs"),I=Y,z=a(()=>w.join(E.homedir(),".claude","kickflip","worktrees"),"worktreesDir"),u=a(e=>{throw new M(e)},"fail");function c(e,...t){const n=T("git",["-C",e,"-c","core.commentChar=;",...t],{encoding:"utf8"});return{ok:n.status===0,out:(n.stdout||"").trim(),err:(n.stderr||n.error?.message||"").trim().split(`
2
+ `)[0]}}a(c,"git");const R=a((e,t)=>c(e,"rev-parse","--short",t).out,"short");function G(e,{repo:t=I(),base:n="main",remote:r=!1,busy:s=[]}={}){const o=w.resolve(t);c(o,"rev-parse","--git-dir").ok||u(`--repo ${o} is not a git repo`),c(o,"check-ref-format","--branch",e).ok||u(`--worktree: "${e}" can't be a branch name`),c(o,"check-ref-format","--branch",n).ok||u("--worktree: invalid base branch");const i=r?`refs/remotes/origin/${n}`:`refs/heads/${n}`;r&&c(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${n}:${i}`),c(o,"rev-parse","--verify","--quiet",i).ok||u(`--worktree: ${o} has no ${n} branch`);const d=w.join(z(),e),$=h.existsSync(d)?V(o,d,e,{ref:i,base:n,busy:s}):null;c(o,"rev-parse","--verify","--quiet",`refs/heads/${e}`).ok&&u(`--worktree: branch ${e} already exists in ${o}`),h.mkdirSync(w.dirname(d),{recursive:!0});const p=c(o,"worktree","add","--quiet","-b",e,d,i);return p.ok||u(`--worktree: git worktree add failed: ${p.err}`),{path:x(d),branch:e,repo:x(o),remade:$}}a(G,"makeWorktree");function V(e,t,n,{ref:r,base:s,busy:o}){const i=a(f=>u(`--worktree: ${t} ${f}; land or remove it first`),"refuse");o.includes(x(t))&&i("already exists and another open builder record uses it");const d=h.realpathSync(t);c(e,"worktree","list","--porcelain").out.split(`
3
3
 
4
- `).some(i=>{const d=i.split(`
5
- `),b=d.find(x=>x.startsWith("worktree "))?.slice(9);try{return d.includes(`branch refs/heads/${t}`)&&p.realpathSync(b)===u}catch{return!1}})||l("already exists");const w=Number(a(r,"rev-list","--count",`${n}..refs/heads/${t}`).out),h=a(e,"status","--porcelain").out.split(`
6
- `).filter(Boolean).length,v=c((i,d)=>`${i} ${d}${i===1?"":"s"}`,"count"),S=[...w?[`${v(w,"commit")} beyond ${o}`]:[],...h?[v(h,"changed file")]:[]];S.length&&l(`holds ${S.join(" and ")}`);const y=a(r,"worktree","remove","--force",e);y.ok||f(`--worktree: could not remove the leftover ${e}: ${y.err}`);const $=a(r,"branch","--quiet","-D",t);return $.ok||f(`--worktree: could not delete the leftover branch ${t}: ${$.err}`),`Remade the leftover worktree ${j(e)} from ${o}: it held no commits or changes.`}c(U,"removeLeftover");function Y(r,{repo:e,manifest:t=_()}={}){const n=r?O(r,t):null;if(!n)return{repo:e??W(),base:"main",remote:!1};const o=q(n.route,t);return{repo:e??(o.repo||f(`/${n.route} has no folder recorded for its session; rejoin it from its repo folder`)),base:o.branch||"main",remote:!o.local}}c(Y,"builderRepo");const $t=c((r,e,{repo:t,busy:n}={})=>H(e,{...Y(r,{repo:t}),busy:n}),"boardWorktree");function X(r){const e=[],t=c(n=>{for(const o of p.readdirSync(n,{withFileTypes:!0}))o.name===".git"||o.name===".repos"||o.name==="node_modules"||(o.isDirectory()?t(k.join(n,o.name)):o.name.endsWith(".test.mjs")&&e.push(k.join(n,o.name)))},"walk");return t(r),e.sort()}c(X,"testFiles");function R(r){const e=X(r);if(!e.length)return{ok:!0,pass:0,fail:0};const{NODE_TEST_CONTEXT:t,...n}=process.env,o=T(process.execPath,["--test","--test-reporter=tap",...e],{cwd:r,env:n,encoding:"utf8",maxBuffer:256*1024*1024}),s=c(u=>Number((o.stdout||"").match(new RegExp(`^# ${u} (\\d+)$`,"m"))?.[1]??0),"count"),l={ok:o.status===0,pass:s("pass"),fail:s("fail")};if(!l.ok){const u=(o.stdout||"")+(o.stderr||"")+(o.error?`
7
- ${o.error.message}
8
- `:""),g=k.join(E.homedir(),".claude","kickflip","logs");try{p.mkdirSync(g,{recursive:!0});const h=k.join(g,`land-${F()}.tap`);p.writeFileSync(h,u,{mode:384}),l.log=h}catch(h){l.logError=`Could not save the full test output: ${h.message}`}l.failures=z(o.stdout||"");const w=(o.stderr||"").trim().split(`
9
- `)[0];l.error=o.error?.message||`${o.signal?`test runner killed by ${o.signal}`:`test runner exited ${o.status}`}${w?`: ${w}`:""}`}return l}c(R,"nodeTests");function z(r){const e=r.split(`
10
- `),t=[],n=[],o=c(s=>s.startsWith("'")?s.slice(1,-1).replaceAll("''","'"):s.startsWith('"')?JSON.parse(s):s,"scalar");for(let s=0;s<e.length;s++){const l=/^(\s*)# Subtest: (.*)$/.exec(e[s]);if(l){const i=l[1].length;for(;t.length&&t.at(-1).indent>=i;)t.pop();t.push({indent:i,name:l[2]})}const u=/^(\s*)not ok \d+ - (.*)$/.exec(e[s]);if(!u||/ # (?:TODO|SKIP)\b/i.test(u[2]))continue;const g=u[1].length,w=[...t.filter(i=>i.indent<g).map(i=>i.name),u[2]].join(" > "),h=[];for(let i=s+1;i<e.length&&e[i].startsWith(" ".repeat(g+2));i++)h.push(e[i]);const v=c(i=>h.find(d=>d.startsWith(`${" ".repeat(g+2)}${i}: `))?.slice(g+i.length+4),"field"),S=v("location"),y=v("error");let $=[];if(y&&/^[|>]/.test(y)){const i=h.findIndex(d=>d.startsWith(`${" ".repeat(g+2)}error: `))+1;for(let d=i;d<h.length&&h[d].startsWith(" ".repeat(g+4));d++)$.push(h[d].slice(g+4))}else y&&($=o(y).split(`
11
- `));if($[0]==="test failed"){const i=[];let d=s-1;for(e[d]?.startsWith("# Subtest: ")&&d--;d>=0&&e[d].startsWith("# ")&&!e[d].startsWith("# Subtest: ");d--)i.unshift(e[d].slice(2));const b=i.findIndex(x=>/^\w*Error(?: \[[^\]]+\])?:/.test(x));b>=0&&($=i.slice(b))}n.push(`${w}
12
- ${S?o(S):"file not reported by test runner"}${$.length?`
13
- `+$.slice(0,3).map(i=>` ${i}`).join(`
14
- `):""}`)}return n}c(z,"tapFailures");function G(r,e){let t;if(process.env.KICKFLIP_RESTART_CMD){try{t=JSON.parse(process.env.KICKFLIP_RESTART_CMD)}catch{}(!Array.isArray(t)||!t.length||!t.every(o=>typeof o=="string"))&&f("KICKFLIP_RESTART_CMD must be a JSON array of strings"),t=[...t,r,...e?[e]:[]]}else{if(r==="hub")return K().restart();t=[process.execPath,L,"restart",e]}const n=T(t[0],t.slice(1),{encoding:"utf8"});return n.status===0?null:(n.stderr||n.error?.message||`exit ${n.status}`).trim().split(`
15
- `)[0]}c(G,"restartRunner");const Q=c(()=>k.join(E.homedir(),".claude","kickflip","land.lock"),"landLock"),V=c(r=>{try{return process.kill(r,0),!0}catch(e){return e.code==="EPERM"}},"alive");async function pt(r,e){const t=Q();for(p.mkdirSync(k.dirname(t),{recursive:!0});;)try{p.writeFileSync(t,JSON.stringify({card:r,pid:process.pid,at:new Date().toISOString()}),{flag:"wx"});break}catch(n){if(n.code!=="EEXIST")throw n;let o=null;try{o=JSON.parse(p.readFileSync(t,"utf8"))}catch{}Number.isInteger(o?.pid)&&V(o.pid)&&f(`${o.card} is landing (since ${o.at}); land again once it finishes`),p.rmSync(t,{force:!0})}try{return await e()}finally{try{JSON.parse(p.readFileSync(t,"utf8")).pid===process.pid&&p.rmSync(t,{force:!0})}catch{}}}c(pt,"holdingLandLock");function Z(r,e,t){const o=a(r,"diff","--name-only","--no-renames","--diff-filter=A",e,t,"--","docs/adr").out.split(`
16
- `).find(s=>/^docs\/adr\/\d{4}-(?!\d{2}-\d{2}-\d{4}-)/.test(s));o&&f(`land: ${o} is named with a number; name it docs/adr/YYYY-MM-DD-HHMM-${k.basename(o).replace(/^\d{4}-/,"")}, the time you wrote it, and land again`)}c(Z,"refuseNumberedAdr");function tt(r,e,t,n){const o=a(r,"log","--format=%h",`${e}..${t}`).out.split(`
17
- `).filter(Boolean);for(const s of o){const l=a(r,"log","-1","--format=%s",s).out;n&&!l.startsWith(`${n}: `)&&f(`land: commit ${s} "${l}" doesn't start with "${n}: "; reword it and land again`),/^# Conflicts:/m.test(a(r,"log","-1","--format=%B",s).out)&&f(`land: commit ${s} "${l}" still holds a "# Conflicts:" note in its message; amend it out and land again`)}}c(tt,"refuseBadMessages");function et(r,{say:e=console.log,runTests:t=R,outcome:n}){const o=t(r);o.ok||f([`tests failed in ${r} (${o.fail} failing); ${n}`,...o.failures?.length?o.failures:[o.error||"test runner failed without test diagnostics"],...o.log?[`Full test output: ${o.log}`]:o.logError?[o.logError]:[]].join(`
18
- `)),e(`tests passed: ${o.pass}`)}c(et,"passTests");function gt({path:r,branch:e,repo:t,base:n,ref:o},{say:s=console.log,runTests:l=R,restart:u=G,boards:g=c(()=>P(),"boards"),clone:w=W()}={}){p.existsSync(r)||f(`no worktree at ${r}`);const h=a(t,"symbolic-ref","--quiet","--short","HEAD").out;if(h!==n&&f(`${t} is on ${h||"a detached HEAD"}, not ${n}; switch it to ${n} (git -C ${t} switch ${n}) and land again`),a(r,"status","--porcelain").out&&f(`${r} has uncommitted changes; commit them there first`),Number(a(t,"rev-list","--count",`${n}..${e}`).out)===0&&f(`${e} has no commits beyond ${n}; nothing to land`),!a(t,"merge-base","--is-ancestor",n,e).ok){if(!a(r,"rebase","--quiet",n).ok){const m=a(r,"diff","--name-only","--diff-filter=U").out.split(`
19
- `).filter(Boolean);a(r,"rebase","--abort"),f(`rebasing ${e} onto ${n} conflicts in ${m.join(", ")||"files git did not name"}; resolve it in ${r} and land again`)}s(`rebased ${e} onto ${n} (${I(t,n)})`)}const v=a(t,"log","--format=%h %s",`${n}..${e}`).out.split(`
20
- `).filter(m=>m&&!m.includes(" "));v.length&&f(`${e} has a commit with an empty subject (${v[0]}); reword it in ${r} and land again`),Z(t,n,e),tt(t,n,e,o),et(r,{say:s,runTests:l,outcome:"nothing landed"});const S=a(t,"diff","--name-only",n,e).out.split(`
21
- `).filter(Boolean),y=a(t,"merge","--quiet","--ff-only",e);y.ok||f(`could not fast-forward ${n} in ${t}: ${y.err}`),s(`${n} fast-forwarded to ${I(t,n)} in ${t}`);const $=[],i=p.realpathSync(t)===p.realpathSync(w)?S.filter(m=>!/\.test\.mjs$/.test(m)):[],d=new Set(i.length?M(k.join(t,"hub","hub.mjs")).files:[]);if(i.some(m=>m.startsWith("hub/")||d.has(m))){const m=u("hub");m?$.push(`the hub didn't restart (${m})`):s("hub restarted, with every site")}else if(i.some(m=>m.startsWith("board/")))for(const m of g()){const C=u("board",m.route);C?$.push(`/${m.route} didn't restart (${C})`):s(`board restarted: ${m.route}`)}const b=a(t,"worktree","remove",r);b.ok?s(`worktree removed: ${r}`):$.push(`could not remove ${r} (${b.err})`);const x=a(t,"branch","--quiet","-d",e);x.ok?s(`branch deleted: ${e}`):$.push(`could not delete branch ${e} (${x.err})`),$.length&&f(`landed, but ${$.join("; ")}`)}c(gt,"land");export{$t as boardWorktree,Y as builderRepo,a as git,pt as holdingLandLock,gt as land,H as makeWorktree,R as nodeTests,et as passTests,G as restartRunner,W as skillRepo,J as worktreesDir};
4
+ `).some(f=>{const l=f.split(`
5
+ `),v=l.find(b=>b.startsWith("worktree "))?.slice(9);try{return l.includes(`branch refs/heads/${n}`)&&h.realpathSync(v)===d}catch{return!1}})||i("already exists");const p=Number(c(e,"rev-list","--count",`${r}..refs/heads/${n}`).out),m=c(t,"status","--porcelain").out.split(`
6
+ `).filter(Boolean).length,g=a((f,l)=>`${f} ${l}${f===1?"":"s"}`,"count"),y=[...p?[`${g(p,"commit")} beyond ${s}`]:[],...m?[g(m,"changed file")]:[]];y.length&&i(`holds ${y.join(" and ")}`);const k=c(e,"worktree","remove","--force",t);k.ok||u(`--worktree: could not remove the leftover ${t}: ${k.err}`);const S=c(e,"branch","--quiet","-D",n);return S.ok||u(`--worktree: could not delete the leftover branch ${n}: ${S.err}`),`Remade the leftover worktree ${x(t)} from ${s}: it held no commits or changes.`}a(V,"removeLeftover");function Z(e,{repo:t,manifest:n=H()}={}){const r=e?_(e,n):null;if(!r)return{repo:t??I(),base:"main",remote:!1};const s=U(r.route,n);return{repo:t??(s.repo||u(`/${r.route} has no folder recorded for its session; rejoin it from its repo folder`)),base:s.branch||"main",remote:!s.local}}a(Z,"builderRepo");const jt=a((e,t,{repo:n,busy:r}={})=>G(t,{...Z(e,{repo:n}),busy:r}),"boardWorktree");function tt(e){const t=[],n=a(r=>{for(const s of h.readdirSync(r,{withFileTypes:!0}))s.name===".git"||s.name===".repos"||s.name==="node_modules"||(s.isDirectory()?n(w.join(r,s.name)):s.name.endsWith(".test.mjs")&&t.push(w.join(r,s.name)))},"walk");return n(e),t.sort()}a(tt,"testFiles");function F(e){const t=tt(e);if(!t.length)return{ok:!0,pass:0,fail:0};const{NODE_TEST_CONTEXT:n,...r}=process.env,s=T(process.execPath,["--test","--test-reporter=tap",...t],{cwd:e,env:r,encoding:"utf8",maxBuffer:256*1024*1024}),o=a(d=>Number((s.stdout||"").match(new RegExp(`^# ${d} (\\d+)$`,"m"))?.[1]??0),"count"),i={ok:s.status===0,pass:o("pass"),fail:o("fail")};if(!i.ok){const d=(s.stdout||"")+(s.stderr||"")+(s.error?`
7
+ ${s.error.message}
8
+ `:""),$=w.join(E.homedir(),".claude","kickflip","logs");try{h.mkdirSync($,{recursive:!0});const m=w.join($,`land-${B()}.tap`);h.writeFileSync(m,d,{mode:384}),i.log=m}catch(m){i.logError=`Could not save the full test output: ${m.message}`}i.failures=et(s.stdout||"");const p=(s.stderr||"").trim().split(`
9
+ `)[0];i.error=s.error?.message||`${s.signal?`test runner killed by ${s.signal}`:`test runner exited ${s.status}`}${p?`: ${p}`:""}`}return i}a(F,"nodeTests");function et(e){const t=e.split(`
10
+ `),n=[],r=[],s=a(o=>o.startsWith("'")?o.slice(1,-1).replaceAll("''","'"):o.startsWith('"')?JSON.parse(o):o,"scalar");for(let o=0;o<t.length;o++){const i=/^(\s*)# Subtest: (.*)$/.exec(t[o]);if(i){const f=i[1].length;for(;n.length&&n.at(-1).indent>=f;)n.pop();n.push({indent:f,name:i[2]})}const d=/^(\s*)not ok \d+ - (.*)$/.exec(t[o]);if(!d||/ # (?:TODO|SKIP)\b/i.test(d[2]))continue;const $=d[1].length,p=[...n.filter(f=>f.indent<$).map(f=>f.name),d[2]].join(" > "),m=[];for(let f=o+1;f<t.length&&t[f].startsWith(" ".repeat($+2));f++)m.push(t[f]);const g=a(f=>m.find(l=>l.startsWith(`${" ".repeat($+2)}${f}: `))?.slice($+f.length+4),"field"),y=g("location"),k=g("error");let S=[];if(k&&/^[|>]/.test(k)){const f=m.findIndex(l=>l.startsWith(`${" ".repeat($+2)}error: `))+1;for(let l=f;l<m.length&&m[l].startsWith(" ".repeat($+4));l++)S.push(m[l].slice($+4))}else k&&(S=s(k).split(`
11
+ `));if(S[0]==="test failed"){const f=[];let l=o-1;for(t[l]?.startsWith("# Subtest: ")&&l--;l>=0&&t[l].startsWith("# ")&&!t[l].startsWith("# Subtest: ");l--)f.unshift(t[l].slice(2));const v=f.findIndex(b=>/^\w*Error(?: \[[^\]]+\])?:/.test(b));v>=0&&(S=f.slice(v))}r.push(`${p}
12
+ ${y?s(y):"file not reported by test runner"}${S.length?`
13
+ `+S.slice(0,3).map(f=>` ${f}`).join(`
14
+ `):""}`)}return r}a(et,"tapFailures");function nt(e,t){let n;if(process.env.KICKFLIP_RESTART_CMD){try{n=JSON.parse(process.env.KICKFLIP_RESTART_CMD)}catch{}(!Array.isArray(n)||!n.length||!n.every(s=>typeof s=="string"))&&u("KICKFLIP_RESTART_CMD must be a JSON array of strings"),n=[...n,e,...t?[t]:[]]}else{if(e==="hub")return X().restart();n=[process.execPath,Q,"restart",t]}const r=T(n[0],n.slice(1),{encoding:"utf8"});return r.status===0?null:(r.stderr||r.error?.message||`exit ${r.status}`).trim().split(`
15
+ `)[0]}a(nt,"restartRunner");const j=a(()=>w.join(E.homedir(),".claude","kickflip"),"kickflipDir"),ot=a(()=>w.join(j(),"landing"),"landing"),C=a(e=>{try{return process.kill(e,0),!0}catch(t){return t.code==="EPERM"}},"alive"),rt=5e3;function N(e){let t=!1,n=null;const r=a(()=>{t=!0,n?.()},"poke"),s=e.map(o=>h.watch(o,r));return{next:a(()=>new Promise(o=>{if(t){t=!1,o();return}const i=setTimeout(()=>{n=null,o()},rt);n=a(()=>{clearTimeout(i),n=null,t=!1,o()},"wake")}),"next"),close:a(()=>{for(const o of s)o.close()},"close")}}a(N,"watching");let st=0;async function W(e,t,n,{onWait:r,asked:s=Date.now()}={}){const o=w.join(j(),`${e}.lock`),i=w.join(j(),`${e}.queue`);h.mkdirSync(i,{recursive:!0});const d=`${String(s).padStart(15,"0")}-${String(st++).padStart(6,"0")}-${process.pid}`,$=N([j(),i]);h.writeFileSync(w.join(i,d),"");let p=null;try{for(;;){const m=h.readdirSync(i).sort().filter(k=>k<d&&C(Number(k.split("-")[2])));let g=null;if(!m.length)try{h.writeFileSync(o,JSON.stringify({card:t,pid:process.pid,at:new Date().toISOString()}),{flag:"wx"});break}catch(k){if(k.code!=="EEXIST")throw k;try{g=JSON.parse(h.readFileSync(o,"utf8"))}catch{}if(!(Number.isInteger(g?.pid)&&C(g.pid))){h.rmSync(o,{force:!0});continue}}const y=g?.card??(m.length?`the ${e} ahead`:null);y&&y!==p&&(p=y,await r?.(y)),await $.next()}}finally{$.close(),h.rmSync(w.join(i,d),{force:!0})}try{return await n()}finally{try{JSON.parse(h.readFileSync(o,"utf8")).pid===process.pid&&h.rmSync(o,{force:!0})}catch{}}}a(W,"holdingQueuedLock");const it=a((e,t,n)=>W("land",e,t,n),"holdingLandLock"),at=a((e,t,n)=>W("test",e,t,n),"holdingTestSlot");async function bt(e,t){const n=ot();h.mkdirSync(n,{recursive:!0});const r=w.join(n,encodeURIComponent(e)),s=N([n]);let o=!1;try{for(;;){if(o&&!h.existsSync(r))return{joined:!0};try{h.writeFileSync(r,JSON.stringify({pid:process.pid}),{flag:"wx"});break}catch(i){if(i.code!=="EEXIST")throw i;let d=null;try{d=JSON.parse(h.readFileSync(r,"utf8"))}catch{}if(!(Number.isInteger(d?.pid)&&C(d.pid))){h.rmSync(r,{force:!0});continue}o=!0,await s.next()}}}finally{s.close()}try{return await t()}finally{h.rmSync(r,{force:!0})}}a(bt,"joiningLand");function ct(e,t,n){const s=c(e,"diff","--name-only","--no-renames","--diff-filter=A",t,n,"--","docs/adr").out.split(`
16
+ `).find(o=>/^docs\/adr\/\d{4}-(?!\d{2}-\d{2}-\d{4}-)/.test(o));s&&u(`land: ${s} is named with a number; name it docs/adr/YYYY-MM-DD-HHMM-${w.basename(s).replace(/^\d{4}-/,"")}, the time you wrote it, and land again`)}a(ct,"refuseNumberedAdr");function lt(e,t,n,r){const s=c(e,"log","--format=%h",`${t}..${n}`).out.split(`
17
+ `).filter(Boolean);for(const o of s){const i=c(e,"log","-1","--format=%s",o).out;r&&!i.startsWith(`${r}: `)&&u(`land: commit ${o} "${i}" doesn't start with "${r}: "; reword it and land again`),/^# Conflicts:/m.test(c(e,"log","-1","--format=%B",o).out)&&u(`land: commit ${o} "${i}" still holds a "# Conflicts:" note in its message; amend it out and land again`)}}a(lt,"refuseBadMessages");function Et(e,t){const n=c(e,"fetch","--quiet","--no-tags","origin",`+refs/heads/${t}:refs/remotes/origin/${t}`);if(n.ok||u(`could not fetch ${t} from origin into ${e}: ${n.err}`),c(e,"symbolic-ref","--quiet","--short","HEAD").out!==t)return;const r=c(e,"merge","--quiet","--ff-only",`refs/remotes/origin/${t}`);r.ok||u(`${t} in ${e} doesn't fast-forward to origin's (${r.err}); bring it level with origin/${t} and land again`)}a(Et,"syncBase");async function ft(e,{say:t=console.log,runTests:n=F,outcome:r,card:s=e,onWait:o}){const i=await at(s,()=>n(e),{onWait:o});i.ok||u([`tests failed in ${e} (${i.fail} failing); ${r}`,...i.failures?.length?i.failures:[i.error||"test runner failed without test diagnostics"],...i.log?[`Full test output: ${i.log}`]:i.logError?[i.logError]:[]].join(`
18
+ `)),t(`tests passed: ${i.pass}`)}a(ft,"passTests");async function q({path:e,branch:t,repo:n,base:r,ref:s},{say:o=console.log,runTests:i=F,onWaitTests:d}={}){if(h.existsSync(e)||u(`no worktree at ${e}`),O(n,r),c(e,"status","--porcelain").out&&u(`${e} has uncommitted changes; commit them there first`),Number(c(n,"rev-list","--count",`${r}..${t}`).out)===0&&u(`${t} has no commits beyond ${r}; nothing to land`),!c(n,"merge-base","--is-ancestor",r,t).ok){const m=c(e,"rebase","--quiet",r);if(!m.ok){const g=c(e,"diff","--name-only","--diff-filter=U").out.split(`
19
+ `).filter(Boolean);c(e,"rebase","--abort"),g.length||u(`rebasing ${t} onto ${r} failed: ${m.err}`),u(`rebasing ${t} onto ${r} conflicts in ${g.join(", ")}; resolve it in ${e} and land again`)}o(`rebased ${t} onto ${r} (${R(n,r)})`)}const $=c(n,"log","--format=%h %s",`${r}..${t}`).out.split(`
20
+ `).filter(m=>m&&!m.includes(" "));$.length&&u(`${t} has a commit with an empty subject (${$[0]}); reword it in ${e} and land again`),ct(n,r,t),lt(n,r,t,s);const p=c(n,"rev-parse",t).out;return c(n,"rev-parse","--verify","--quiet",D(t)).out===p?(o(`${t} was tested on ${r} as it is now; not testing it again`),p):(await ft(e,{say:o,runTests:i,outcome:"nothing landed",card:s??t,onWait:d}),c(n,"update-ref",D(t),p),p)}a(q,"prepareLand");const D=a(e=>`refs/kickflip/tested/${e}`,"testedRef");function O(e,t){const n=c(e,"symbolic-ref","--quiet","--short","HEAD").out;n!==t&&u(`${e} is on ${n||"a detached HEAD"}, not ${t}; switch it to ${t} (git -C ${e} switch ${t}) and land again`)}a(O,"onBase");async function Tt(e,{hold:t=!1,onWait:n,onWaitTests:r,sync:s=a(()=>{},"sync"),...o}={}){const{say:i=console.log}=o,d=Date.now();for(;;){const $=await q(e,{...o,onWaitTests:r});if(t||await it(e.ref??e.branch,()=>(s(),A(e,$,o)),{onWait:n,asked:d}))return;i(`${e.base} moved while ${e.branch} was tested; testing it on ${e.base} again`)}}a(Tt,"landInTurn");async function It(e,{hold:t=!1,...n}={}){const r=await q(e,n);t||A(e,r,n)||u(`${e.base} moved while ${e.branch} was tested; land again`)}a(It,"land");function A({path:e,branch:t,repo:n,base:r},s,{say:o=console.log,restart:i=nt,boards:d=a(()=>J(),"boards"),clone:$=I()}={}){if(O(n,r),c(n,"rev-parse",t).out!==s||!c(n,"merge-base","--is-ancestor",r,s).ok)return!1;const p=c(n,"diff","--name-only",r,t).out.split(`
21
+ `).filter(Boolean),m=c(n,"merge","--quiet","--ff-only",s);m.ok||u(`could not fast-forward ${r} in ${n}: ${m.err}`),o(`${r} fast-forwarded to ${R(n,r)} in ${n}`);const g=[],y=h.realpathSync(n)===h.realpathSync($)?p.filter(l=>!/\.test\.mjs$/.test(l)):[],k=new Set(y.length?K(w.join(n,"hub","hub.mjs")).files:[]);if(y.some(l=>l.startsWith("hub/")||k.has(l))){const l=i("hub");l?g.push(`the hub didn't restart (${l})`):o("hub restarted, with every site")}else if(y.some(l=>l.startsWith("board/")))for(const l of d()){const v=i("board",l.route);v?g.push(`/${l.route} didn't restart (${v})`):o(`board restarted: ${l.route}`)}const S=c(n,"worktree","remove",e);S.ok?o(`worktree removed: ${e}`):g.push(`could not remove ${e} (${S.err})`);const f=c(n,"branch","--quiet","-d",t);return f.ok?o(`branch deleted: ${t}`):g.push(`could not delete branch ${t} (${f.err})`),c(n,"update-ref","-d",D(t)),g.length&&u(`landed, but ${g.join("; ")}`),!0}a(A,"landPrepared");export{jt as boardWorktree,Z as builderRepo,c as git,it as holdingLandLock,at as holdingTestSlot,bt as joiningLand,It as land,Tt as landInTurn,A as landPrepared,G as makeWorktree,F as nodeTests,ft as passTests,q as prepareLand,nt as restartRunner,I as skillRepo,Et as syncBase,z as worktreesDir};
package/hub/boards.mjs CHANGED
@@ -1,4 +1,4 @@
1
- var z=Object.defineProperty;var o=(e,t)=>z(e,"name",{value:t,configurable:!0});import p from"node:fs";import c from"node:path";import Z from"node:os";import q from"node:crypto";import{fileURLToPath as Q}from"node:url";import{routeRecord as d}from"./routes.mjs";import{deliver as X,readSettings as Y}from"./push.mjs";import{SLOWEST_CHECK_MINUTES as ee}from"./schedule.mjs";import{liveSessions as y,ps as te}from"./live.mjs";import{withFileLock as ne}from"./locks.mjs";import{dropSessionPrompts as se,readPrompts as re,savePrompts as oe,withPromptsLock as ie}from"./prompts.mjs";import{storedPath as v}from"../bin/install.mjs";import{hubService as ae}from"../shared/service.mjs";import"../shared/repos.mjs";import"./repos.mjs";import{reviewLinks as ce,topicItems as ue}from"../board/review.mjs";import{cardPRs as le}from"../shared/card-prs.mjs";const fe=c.resolve(c.dirname(Q(import.meta.url)),".."),I=c.join(Z.homedir(),".claude"),w=c.join(I,"kickflip"),C=c.join(I,"waves"),O=c.join(w,"manifest.json"),pe=c.join(w,"github-state.json"),de=c.join(w,"wake-state.json"),it=c.join(w,"incoming-files.json"),F=c.join(w,"deaf-state.json"),x=c.join(w,"resume-state.json"),E=c.join(w,"link-faults.json"),he=c.join(w,"repo-fetch.json"),P=c.join(w,"hub-code.json"),me=["merged","dropped"],we="1970-01-01T00:00:00Z",ge=2*6e4,ke=15*6e4,f=o((e,t)=>{try{return JSON.parse(p.readFileSync(e,"utf8"))}catch{return t}},"readJSON");function k(e,t){const n=`${e}.${process.pid}.tmp`;p.mkdirSync(c.dirname(e),{recursive:!0}),p.writeFileSync(n,JSON.stringify(t,null,2)+`
2
- `),p.renameSync(n,e)}o(k,"saveJSON");const j=o(()=>f(O,{sites:{}}),"readManifest"),at=o(e=>ne(O,e),"withManifestLock");function ct(e){return ie(()=>{const t=[...Object.values(e.sites||{}),...Object.values(e.closed||{}).map(a=>a.site)];for(const a of t.filter(Boolean))a.dir&&(a.dir=v(a.dir)),Array.isArray(a.cmd)&&(a.cmd=a.cmd.map(l=>v(l))),a.owner?.repo&&(a.owner.repo=v(a.owner.repo));for(const a of Object.values(e.starting||{}))a.repo&&(a.repo=v(a.repo));const n=j(),s=o((a,l)=>d(a.sites,l)?.owner||d(a.starting,l),"owner"),r=new Set([...Object.keys(n.sites||{}),...Object.keys(e.sites||{}),...Object.keys(n.starting||{}),...Object.keys(e.starting||{})]),i=[];for(const a of r){const l=s(n,a),u=s(e,a);(l?.session!==u?.session||l?.since!==u?.since)&&i.push(l?.session,u?.session)}if(i.some(Boolean)){const a=re();se(a,i),oe(a)}k(O,e)})}o(ct,"writeManifest");const $=o(e=>`${e.repo}#${e.pr}`,"prKey"),M=o((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),B=o(e=>(Array.isArray(e)?e:[]).map(t=>typeof t=="string"?t:t?.name).filter(t=>typeof t=="string"&&t),"names"),je=o(e=>Object.fromEntries((Array.isArray(e)?e:[]).filter(t=>typeof t?.github=="string"&&t.github&&typeof t.name=="string"&&t.name).map(t=>[t.github.toLowerCase(),t.name])),"reviewerLogins"),H=o(e=>Array.isArray(e?.items),"isWave"),U=/board[/\\]server\.mjs$/,A=o(e=>Array.isArray(e?.cmd)&&e.cmd.some(t=>U.test(t))&&typeof e.dir=="string","isBoardSite"),ut=o(e=>e.local===!0||!(e.github||e.remote),"buildsLocally");function be(e,t,n){if(n==="file"){const r=f(c.resolve(e,t.items),null);return H(r)?[{slug:t.slug||c.basename(t.items,".json"),wave:r}]:[]}let s=[];try{s=p.readdirSync(C,{withFileTypes:!0}).filter(r=>r.isDirectory()).map(r=>r.name).sort()}catch{}return s.map(r=>({slug:r,wave:f(c.join(C,r,"wave.json"),null)})).filter(r=>H(r.wave))}o(be,"wavesOf");function ye(e,t){return e.flatMap(({slug:n,wave:s})=>s.items.filter(r=>!me.includes(r?.status)).flatMap(r=>le(r,t||s.repo).map(i=>({wave:n,id:r.id,pr:i.number,repo:i.repo,since:r.gh_since||we}))))}o(ye,"prsOf");function _(e,t){const n=f(c.join(t.dir,"board.json"),{}),s=typeof n.items=="string"&&n.items!=="waves"?"file":"waves",r=be(t.dir,n,s),i=t.github;return{route:e,dir:t.dir,port:t.port,title:n.title||t.title||e,kind:s,waves:r,prs:ye(r,i),github:i,...t.remote&&!i?{remote:t.remote}:{},branch:t.branch,auto_build:n.auto_build===!0,auto_review:n.auto_review===!0,skipDraft:n.skip_draft===!0,githubLogin:typeof n.github_login=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(n.github_login)?n.github_login:null,tickets:n.tickets,reviewers:B(n.reviewers),reviewerLogins:je(n.reviewers),owner:t.owner||null,watch:Array.isArray(t.watch)&&t.watch.length?t.watch:["data/events.jsonl"],filter:t.filter||null}}o(_,"boardOf");function T(e=j()){return Object.entries(e?.sites||{}).filter(([,t])=>A(t)).map(([t,n])=>_(t,n))}o(T,"listBoards");function lt(e=j()){const t=Object.entries(e?.sites||{}).find(([,n])=>n?.hub===!0&&A(n));return t?_(...t):void 0}o(lt,"hubBoard");function ve(e,t){const n=o(s=>{try{return p.realpathSync(s)}catch{return c.resolve(s)}},"real");return c.resolve(e)===c.resolve(t)||n(e)===n(t)}o(ve,"samePath");function ft(e,t=j()){const n=Object.entries(t?.sites||{}).find(([,s])=>A(s)&&ve(s.dir,e));return n?_(...n):null}o(ft,"boardAt");async function J(e,t,n){try{const r=await fetch(`http://127.0.0.1:${e.port}/_claude/events`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({kind:t,...n}),signal:AbortSignal.timeout(5e3)});if(r.ok)return(await r.json()).id}catch{}const s={id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),ts:new Date().toISOString(),kind:t,data:n};return p.mkdirSync(c.join(e.dir,"data"),{recursive:!0}),p.appendFileSync(c.join(e.dir,"data","events.jsonl"),JSON.stringify(s)+`
3
- `),s.id}o(J,"appendEvent");async function pt(e,t,n){const s=[],r=[];for(const i of e)try{s.push({route:i.route,id:await J(i,t,n)})}catch(a){r.push(`${i.route}: could not add the ${t} event (${a.message})`)}return{events:s,failed:r}}o(pt,"sendToBoards");function Se(e,t=Date.now(),{grace:n=ge}={}){const s=f(c.join(e,"data","watcher.json"),null);if(!Number.isInteger(s?.pid))return!1;if(typeof s.exited_at=="string")return t-Date.parse(s.exited_at)<=n;try{process.kill(s.pid,0)}catch(r){if(r.code!=="EPERM")return!1}return W(s.pid)?.includes("wait-events.mjs")??!0}o(Se,"watcherRunning");const W=o(e=>te(["-p",String(e),"-o","command="]),"commandOf"),G=o(e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"escaped");function dt(e,t,n,{command:s=W,kill:r=process.kill}={}){const i=f(c.join(e,"data","watcher.json"),null);if(!Number.isInteger(i?.pid)||i.pid===process.pid||typeof i.exited_at=="string"||!new RegExp(`wait-events\\.mjs['"]?\\s+['"]?${G(t)}['"]?\\s+['"]?${G(n)}['"]?(\\s|$)`).test(s(i.pid)??""))return null;try{r(i.pid,"SIGTERM")}catch{return null}return i.pid}o(dt,"stopPreviousWatcher");function Oe(e,{now:t=Date.now(),running:n=y()}={}){const s=e.owner?.session;return!!(s&&(n?.has(s)??!0))&&Se(e.dir,t)}o(Oe,"isListening");function N(e,{now:t=Date.now(),running:n=y(),wakes:s=f(x,{})}={}){if(Oe(e,{now:t,running:n}))return"listening";const r=e.owner?.session,i=d(s,e.route);return!r||!e.owner.repo||i?.error||i?.pushed_at?"deaf":i?.resumed_at?"waking":n?.has(r)?"deaf":"asleep"}o(N,"listenState");const L=o((e,t)=>c.join(e.dir,"data",`.watch-cursor-${t}.json`),"cursorPath"),S=o(e=>L(e,e.owner.session),"cursorFile");function ht(e){const t=e.owner?.session?f(S(e),{}):{};return Object.fromEntries(e.watch.map(n=>{const s=d(t,n);return[n,Number.isSafeInteger(s)&&s>=0?s:0]}))}o(ht,"watcherCursor");const mt=o(e=>Object.fromEntries(Object.entries(R(e)).map(([t,n])=>[t,n.length])),"watchedCounts");function wt(e,{from:t,to:n=t,mark:s=null}){const r=f(L(e,t),null);if(!r&&!s)return;const i=Object.assign(Object.create(null),r);for(const[a,l]of Object.entries(s||{}))i[a]=Math.min(d(i,a)??l,l);k(L(e,n),i)}o(wt,"carryCursor");function R(e){const t=e.filter?new RegExp(e.filter):null;return Object.fromEntries(e.watch.map(n=>{let s=[];try{s=p.readFileSync(c.resolve(e.dir,n),"utf8").split(`
4
- `).filter(Boolean)}catch{}return[n,t?s.filter(r=>t.test(r)):s]}))}o(R,"watchedLines");function xe(e){if(!e.owner?.session)return null;const t=f(S(e),null),n=t?0:Date.parse(e.owner.since||"")||0;let s=null;for(const[r,i]of Object.entries(R(e)))for(const a of t?i.slice(d(t,r)||0):i){let l=null;try{l=JSON.parse(a).ts}catch{}const u=Date.parse(l);Number.isNaN(u)||u<n||(!s||u<Date.parse(s))&&(s=l)}return s}o(xe,"oldestUnheard");async function gt({manifest:e,now:t=new Date,running:n=y()}={}){if(!n)return{pushed:[],failed:[]};const s=Object.assign(Object.create(null),f(F,{})),r=[],i=[];let a=!1;const l=f(x,{});for(const u of T(e)){const h=N(u,{now:t.getTime(),running:n,wakes:l});if(h==="listening"){d(s,u.route)&&(delete s[u.route],a=!0);continue}if(h==="asleep"||h==="waking"||d(s,u.route))continue;const m=xe(u);if(!m||t-Date.parse(m)<ke)continue;const g=Math.round((t-Date.parse(m))/6e4),K=n.has(u.owner.session)?"no watcher runs. Relaunch it in the board's session.":"its session isn't running, and the hub can't wake it.";try{await X({title:`${u.title} isn't listening`,body:`An event has waited ${g} min and ${K}`,url:`/${u.route}/`,tag:`deaf-${u.route}`},{kind:"fault",immediate:!0,at:t}),s[u.route]={at:t.toISOString(),since:m},a=!0,r.push(u.route)}catch(V){i.push(`${u.route}: could not push that it isn't listening (${V.message})`)}}return a&&k(F,s),{pushed:r,failed:i}}o(gt,"checkListening");const Ee=o(e=>c.join(e,"data","handshake.json"),"handshakeFile");async function kt(e,t=j()){const n=T(t).find(r=>r.route===e);if(!n)throw new Error(`/${e} is not a board site in the manifest`);n.owner?.session&&!p.existsSync(S(n))&&k(S(n),Object.fromEntries(Object.entries(R(n)).map(([r,i])=>[r,i.length])));const s={id:await J(n,"hello",{title:n.title}),at:new Date().toISOString(),session:n.owner?.session||null};return k(Ee(n.dir),{hello:s,ack:null}),{route:e,hello:s.id}}o(kt,"handshake");const jt=["github","repo-fetch","events","wakes","slack"];function $e(e,t){if(e.remote)return null;const n=t?.lastCheck;if(!n?.at)return{link:"github",ok:!1,detail:"The hub hasn't checked GitHub yet."};const s=e.prs.filter(i=>t.prs?.[$(i)]?.at!==n.at).map(i=>`#${i.pr}`);if(s.length)return{link:"github",ok:!1,detail:`${[...new Set(s)].join(", ")} missed the last check.`};if(!(n.boards||[]).includes(e.route))return{link:"github",ok:!1,detail:"The last check didn't reach this board."};const r=new Set(e.prs.map($)).size;return{link:"github",ok:!0,detail:r?`${M(r,"open PR")}, all in the last check.`:"No open PRs; the last check reached this board."}}o($e,"githubLink");function Ae(e){return e?e.ok?{link:"repo-fetch",ok:!0,detail:"The hub's last git fetch of this board's repo worked."}:{link:"repo-fetch",ok:!1,detail:`${e.error.replace(/[.\s]*$/,".")} ${e.hint}.`}:null}o(Ae,"repoFetchLink");const _e={listening:{ok:!0,detail:"Its session and a watcher are running."},waking:{ok:!0,detail:"Waking: the hub resumed its session, which listens again once it rejoins."},asleep:{ok:!0,detail:"Asleep: its session idled out, and the hub wakes it on the next event or a tap on its page."},deaf:{ok:!1,detail:"Not listening: its session or its watcher isn't running, and the hub can't wake it, so events wait."}},Te=o(e=>({link:"events",listen:e,..._e[e]}),"eventsLink");function Ne(e,t,n){return(n?.last?.failed||[]).includes(e.route)?{link:"wakes",ok:!1,detail:`The last wake (${n.last.due.join(", ")}) couldn't reach this board.`}:{link:"wakes",ok:!0,detail:`Daily at ${t.quiet.end}, when quiet hours end.`}}o(Ne,"wakesLink");function Le(e,t,n){const s=[];let r=0;for(const i of e.prs){const a=t?.prs?.[$(i)];if(a?.state!=="OPEN"||a.isDraft)continue;r++;const{wave:l}=e.waves.find(g=>g.slug===i.wave),u=l.items.find(g=>g.id===i.id),h=ce(l,u,`${i.wave}/${i.id}`,n),m=[...new Set([...e.reviewers,...B(l.reviewers)])].filter(g=>!Object.hasOwn(h,g));m.length&&s.push(`#${i.pr} has no link for ${m.join(", ")}`)}return s.length?{link:"slack",ok:!1,detail:`${s.join("; ")}.`}:{link:"slack",ok:!0,detail:r?`Every reviewer has the link to ${M(r,"PR")} in review.`:"No PR is in review."}}o(Le,"slackLink");const Re=/\b(?:from|import)\s*['"](\.\.?\/[^'"]+)['"]/g;function D(e=c.join(fe,"hub","hub.mjs")){const t=c.dirname(c.dirname(e)),n=new Map,s=o(a=>{if(n.has(a))return;let l;try{l=p.readFileSync(a)}catch{return}n.set(a,l);for(const u of l.toString("utf8").matchAll(Re))s(c.resolve(c.dirname(a),u[1]))},"visit");s(e);const r=[...n.keys()].map(a=>c.relative(t,a)).sort(),i=q.createHash("sha256");for(const a of r)i.update(a).update("\0").update(n.get(c.join(t,a))).update("\0");return{hash:i.digest("hex").slice(0,12),files:r}}o(D,"hubCode");const bt=o(e=>D(c.resolve(e.cwd||e.dir,e.cmd.find(t=>U.test(t)))).hash,"boardCode");function yt(){const e={...D(),pid:process.pid,at:new Date().toISOString()};return k(P,e),e}o(yt,"recordHubCode");const De=ae().restartCommand,Ie=o(()=>f(P,null)?.hash??null,"runningHubCode"),Ce=o(()=>({running:Ie(),onDisk:D().hash}),"hubCodeState"),vt=o(({running:e,onDisk:t}=Ce())=>e&&e===t?{link:"hub",ok:!0,detail:`The hub runs the code in its files (${t}).`}:{link:"hub",ok:!1,detail:`The hub runs older code than its files; restart it with ${De}`},"hubLink"),b=6e4,Fe={github:o(e=>(e+10)*b,"github"),"repo-fetch":o(e=>(e+10)*b,"repo-fetch"),events:o(()=>2*b,"events"),wakes:o(e=>(e+10)*b,"wakes"),slack:o(e=>(e+10)*b,"slack")};function Pe(e,t,{now:n,faults:s}){const r=s===void 0,i=r?d(f(E,{}),e)||{}:s,a={},l=t.map(u=>{if(u.ok)return{...u,state:"ok"};const h=d(i,u.link)||new Date(n).toISOString();a[u.link]=h;const m=Fe[u.link](ee);return{...u,state:n-Date.parse(h)>=m?"broken":"settling",since:h}});if(r&&JSON.stringify(a)!==JSON.stringify(i)){const u=Object.assign(Object.create(null),f(E,{}));Object.keys(a).length?u[e]=a:delete u[e];try{k(E,u)}catch{}}return l}o(Pe,"settle");function Me(e,t={}){const n=t.github??f(pe,{}),s=t.settings??Y(),r=[$e(e,n),Ae(d(t.fetches??f(he,{}),e.route)),Te(t.listen??N(e)),Ne(e,s,t.wakes??f(de,{})),Le(e,n,t.topics??ue())].filter(Boolean);return Pe(e.route,r,{now:t.now??Date.now(),faults:t.faults})}o(Me,"linksOf");function St(e){const t={running:y(),wakes:f(x,{})};if(t.running)for(const n of T(e))Me(n,{listen:N(n,t)})}o(St,"recordLinkFaults");export{ke as DEAF_AFTER,pe as GITHUB_STATE,Fe as GRACE,P as HUB_CODE,it as INCOMING_FILES,jt as LINKS,he as REPO_FETCH,x as RESUME_STATE,de as WAKE_STATE,C as WAVES,J as appendEvent,ft as boardAt,bt as boardCode,ut as buildsLocally,wt as carryCursor,gt as checkListening,kt as handshake,Ee as handshakeFile,lt as hubBoard,D as hubCode,Ce as hubCodeState,vt as hubLink,A as isBoardSite,Oe as isListening,Me as linksOf,T as listBoards,N as listenState,xe as oldestUnheard,$ as prKey,ye as prsOf,f as readJSON,j as readManifest,yt as recordHubCode,St as recordLinkFaults,je as reviewerLogins,Ie as runningHubCode,ve as samePath,k as saveJSON,pt as sendToBoards,dt as stopPreviousWatcher,mt as watchedCounts,R as watchedLines,ht as watcherCursor,Se as watcherRunning,at as withManifestLock,ct as writeManifest};
1
+ var z=Object.defineProperty;var o=(e,t)=>z(e,"name",{value:t,configurable:!0});import p from"node:fs";import c from"node:path";import Z from"node:os";import q from"node:crypto";import{fileURLToPath as Q}from"node:url";import{routeRecord as d}from"./routes.mjs";import{deliver as X,readSettings as Y}from"./push.mjs";import{SLOWEST_CHECK_MINUTES as ee}from"./schedule.mjs";import{liveSessions as y,ps as te}from"./live.mjs";import{withFileLock as ne}from"./locks.mjs";import{dropSessionPrompts as se,readPrompts as re,savePrompts as oe,withPromptsLock as ie}from"./prompts.mjs";import{storedPath as v}from"../bin/install.mjs";import{hubService as ae}from"../shared/service.mjs";import"../shared/repos.mjs";import"./repos.mjs";import{reviewLinks as ce,topicItems as ue}from"../board/review.mjs";import{cardPRs as le}from"../shared/card-prs.mjs";const fe=c.resolve(c.dirname(Q(import.meta.url)),".."),I=c.join(Z.homedir(),".claude"),w=c.join(I,"kickflip"),C=c.join(I,"waves"),O=c.join(w,"manifest.json"),pe=c.join(w,"github-state.json"),de=c.join(w,"wake-state.json"),at=c.join(w,"incoming-files.json"),F=c.join(w,"deaf-state.json"),x=c.join(w,"resume-state.json"),E=c.join(w,"link-faults.json"),he=c.join(w,"repo-fetch.json"),P=c.join(w,"hub-code.json"),me=["merged","dropped"],we="1970-01-01T00:00:00Z",ge=2*6e4,ke=15*6e4,f=o((e,t)=>{try{return JSON.parse(p.readFileSync(e,"utf8"))}catch{return t}},"readJSON");function k(e,t){const n=`${e}.${process.pid}.tmp`;p.mkdirSync(c.dirname(e),{recursive:!0}),p.writeFileSync(n,JSON.stringify(t,null,2)+`
2
+ `),p.renameSync(n,e)}o(k,"saveJSON");const j=o(()=>f(O,{sites:{}}),"readManifest"),ct=o(e=>ne(O,e),"withManifestLock");function ut(e){return ie(()=>{const t=[...Object.values(e.sites||{}),...Object.values(e.closed||{}).map(a=>a.site)];for(const a of t.filter(Boolean))a.dir&&(a.dir=v(a.dir)),Array.isArray(a.cmd)&&(a.cmd=a.cmd.map(l=>v(l))),a.owner?.repo&&(a.owner.repo=v(a.owner.repo));for(const a of Object.values(e.starting||{}))a.repo&&(a.repo=v(a.repo));const n=j(),s=o((a,l)=>d(a.sites,l)?.owner||d(a.starting,l),"owner"),r=new Set([...Object.keys(n.sites||{}),...Object.keys(e.sites||{}),...Object.keys(n.starting||{}),...Object.keys(e.starting||{})]),i=[];for(const a of r){const l=s(n,a),u=s(e,a);(l?.session!==u?.session||l?.since!==u?.since)&&i.push(l?.session,u?.session)}if(i.some(Boolean)){const a=re();se(a,i),oe(a)}k(O,e)})}o(ut,"writeManifest");const $=o(e=>`${e.repo}#${e.pr}`,"prKey"),M=o((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),B=o(e=>(Array.isArray(e)?e:[]).map(t=>typeof t=="string"?t:t?.name).filter(t=>typeof t=="string"&&t),"names"),je=o(e=>Object.fromEntries((Array.isArray(e)?e:[]).filter(t=>typeof t?.github=="string"&&t.github&&typeof t.name=="string"&&t.name).map(t=>[t.github.toLowerCase(),t.name])),"reviewerLogins"),H=o(e=>Array.isArray(e?.items),"isWave"),U=/board[/\\]server\.mjs$/,A=o(e=>Array.isArray(e?.cmd)&&e.cmd.some(t=>U.test(t))&&typeof e.dir=="string","isBoardSite"),be=o(e=>e.local===!0||!(e.github||e.remote),"buildsLocally");function ye(e,t,n){if(n==="file"){const r=f(c.resolve(e,t.items),null);return H(r)?[{slug:t.slug||c.basename(t.items,".json"),wave:r}]:[]}let s=[];try{s=p.readdirSync(C,{withFileTypes:!0}).filter(r=>r.isDirectory()).map(r=>r.name).sort()}catch{}return s.map(r=>({slug:r,wave:f(c.join(C,r,"wave.json"),null)})).filter(r=>H(r.wave))}o(ye,"wavesOf");function ve(e,t){return e.flatMap(({slug:n,wave:s})=>s.items.filter(r=>!me.includes(r?.status)).flatMap(r=>le(r,t||s.repo).map(i=>({wave:n,id:r.id,pr:i.number,repo:i.repo,since:r.gh_since||we}))))}o(ve,"prsOf");function _(e,t){const n=f(c.join(t.dir,"board.json"),{}),s=typeof n.items=="string"&&n.items!=="waves"?"file":"waves",r=ye(t.dir,n,s),i=t.github;return{route:e,dir:t.dir,port:t.port,title:n.title||t.title||e,kind:s,waves:r,prs:ve(r,i),github:i,...t.remote&&!i?{remote:t.remote}:{},branch:t.branch,...be(t)?{local:!0}:{},auto_build:n.auto_build===!0,auto_review:n.auto_review===!0,skipDraft:n.skip_draft===!0,autoMerge:n.auto_merge===!0,githubLogin:typeof n.github_login=="string"&&/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(n.github_login)?n.github_login:null,tickets:n.tickets,reviewers:B(n.reviewers),reviewerLogins:je(n.reviewers),owner:t.owner||null,watch:Array.isArray(t.watch)&&t.watch.length?t.watch:["data/events.jsonl"],filter:t.filter||null}}o(_,"boardOf");function T(e=j()){return Object.entries(e?.sites||{}).filter(([,t])=>A(t)).map(([t,n])=>_(t,n))}o(T,"listBoards");function lt(e=j()){const t=Object.entries(e?.sites||{}).find(([,n])=>n?.hub===!0&&A(n));return t?_(...t):void 0}o(lt,"hubBoard");function Se(e,t){const n=o(s=>{try{return p.realpathSync(s)}catch{return c.resolve(s)}},"real");return c.resolve(e)===c.resolve(t)||n(e)===n(t)}o(Se,"samePath");function ft(e,t=j()){const n=Object.entries(t?.sites||{}).find(([,s])=>A(s)&&Se(s.dir,e));return n?_(...n):null}o(ft,"boardAt");async function J(e,t,n){try{const r=await fetch(`http://127.0.0.1:${e.port}/_claude/events`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({kind:t,...n}),signal:AbortSignal.timeout(5e3)});if(r.ok)return(await r.json()).id}catch{}const s={id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),ts:new Date().toISOString(),kind:t,data:n};return p.mkdirSync(c.join(e.dir,"data"),{recursive:!0}),p.appendFileSync(c.join(e.dir,"data","events.jsonl"),JSON.stringify(s)+`
3
+ `),s.id}o(J,"appendEvent");async function pt(e,t,n){const s=[],r=[];for(const i of e)try{s.push({route:i.route,id:await J(i,t,n)})}catch(a){r.push(`${i.route}: could not add the ${t} event (${a.message})`)}return{events:s,failed:r}}o(pt,"sendToBoards");function Oe(e,t=Date.now(),{grace:n=ge}={}){const s=f(c.join(e,"data","watcher.json"),null);if(!Number.isInteger(s?.pid))return!1;if(typeof s.exited_at=="string")return t-Date.parse(s.exited_at)<=n;try{process.kill(s.pid,0)}catch(r){if(r.code!=="EPERM")return!1}return W(s.pid)?.includes("wait-events.mjs")??!0}o(Oe,"watcherRunning");const W=o(e=>te(["-p",String(e),"-o","command="]),"commandOf"),G=o(e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"escaped");function dt(e,t,n,{command:s=W,kill:r=process.kill}={}){const i=f(c.join(e,"data","watcher.json"),null);if(!Number.isInteger(i?.pid)||i.pid===process.pid||typeof i.exited_at=="string"||!new RegExp(`wait-events\\.mjs['"]?\\s+['"]?${G(t)}['"]?\\s+['"]?${G(n)}['"]?(\\s|$)`).test(s(i.pid)??""))return null;try{r(i.pid,"SIGTERM")}catch{return null}return i.pid}o(dt,"stopPreviousWatcher");function xe(e,{now:t=Date.now(),running:n=y()}={}){const s=e.owner?.session;return!!(s&&(n?.has(s)??!0))&&Oe(e.dir,t)}o(xe,"isListening");function N(e,{now:t=Date.now(),running:n=y(),wakes:s=f(x,{})}={}){if(xe(e,{now:t,running:n}))return"listening";const r=e.owner?.session,i=d(s,e.route);return!r||!e.owner.repo||i?.error||i?.pushed_at?"deaf":i?.resumed_at?"waking":n?.has(r)?"deaf":"asleep"}o(N,"listenState");const L=o((e,t)=>c.join(e.dir,"data",`.watch-cursor-${t}.json`),"cursorPath"),S=o(e=>L(e,e.owner.session),"cursorFile");function ht(e){const t=e.owner?.session?f(S(e),{}):{};return Object.fromEntries(e.watch.map(n=>{const s=d(t,n);return[n,Number.isSafeInteger(s)&&s>=0?s:0]}))}o(ht,"watcherCursor");const mt=o(e=>Object.fromEntries(Object.entries(R(e)).map(([t,n])=>[t,n.length])),"watchedCounts");function wt(e,{from:t,to:n=t,mark:s=null}){const r=f(L(e,t),null);if(!r&&!s)return;const i=Object.assign(Object.create(null),r);for(const[a,l]of Object.entries(s||{}))i[a]=Math.min(d(i,a)??l,l);k(L(e,n),i)}o(wt,"carryCursor");function R(e){const t=e.filter?new RegExp(e.filter):null;return Object.fromEntries(e.watch.map(n=>{let s=[];try{s=p.readFileSync(c.resolve(e.dir,n),"utf8").split(`
4
+ `).filter(Boolean)}catch{}return[n,t?s.filter(r=>t.test(r)):s]}))}o(R,"watchedLines");function Ee(e){if(!e.owner?.session)return null;const t=f(S(e),null),n=t?0:Date.parse(e.owner.since||"")||0;let s=null;for(const[r,i]of Object.entries(R(e)))for(const a of t?i.slice(d(t,r)||0):i){let l=null;try{l=JSON.parse(a).ts}catch{}const u=Date.parse(l);Number.isNaN(u)||u<n||(!s||u<Date.parse(s))&&(s=l)}return s}o(Ee,"oldestUnheard");async function gt({manifest:e,now:t=new Date,running:n=y()}={}){if(!n)return{pushed:[],failed:[]};const s=Object.assign(Object.create(null),f(F,{})),r=[],i=[];let a=!1;const l=f(x,{});for(const u of T(e)){const h=N(u,{now:t.getTime(),running:n,wakes:l});if(h==="listening"){d(s,u.route)&&(delete s[u.route],a=!0);continue}if(h==="asleep"||h==="waking"||d(s,u.route))continue;const m=Ee(u);if(!m||t-Date.parse(m)<ke)continue;const g=Math.round((t-Date.parse(m))/6e4),K=n.has(u.owner.session)?"no watcher runs. Relaunch it in the board's session.":"its session isn't running, and the hub can't wake it.";try{await X({title:`${u.title} isn't listening`,body:`An event has waited ${g} min and ${K}`,url:`/${u.route}/`,tag:`deaf-${u.route}`},{kind:"fault",immediate:!0,at:t}),s[u.route]={at:t.toISOString(),since:m},a=!0,r.push(u.route)}catch(V){i.push(`${u.route}: could not push that it isn't listening (${V.message})`)}}return a&&k(F,s),{pushed:r,failed:i}}o(gt,"checkListening");const $e=o(e=>c.join(e,"data","handshake.json"),"handshakeFile");async function kt(e,t=j()){const n=T(t).find(r=>r.route===e);if(!n)throw new Error(`/${e} is not a board site in the manifest`);n.owner?.session&&!p.existsSync(S(n))&&k(S(n),Object.fromEntries(Object.entries(R(n)).map(([r,i])=>[r,i.length])));const s={id:await J(n,"hello",{title:n.title}),at:new Date().toISOString(),session:n.owner?.session||null};return k($e(n.dir),{hello:s,ack:null}),{route:e,hello:s.id}}o(kt,"handshake");const jt=["github","repo-fetch","events","wakes","slack"];function Ae(e,t){if(e.remote)return null;const n=t?.lastCheck;if(!n?.at)return{link:"github",ok:!1,detail:"The hub hasn't checked GitHub yet."};const s=e.prs.filter(i=>t.prs?.[$(i)]?.at!==n.at).map(i=>`#${i.pr}`);if(s.length)return{link:"github",ok:!1,detail:`${[...new Set(s)].join(", ")} missed the last check.`};if(!(n.boards||[]).includes(e.route))return{link:"github",ok:!1,detail:"The last check didn't reach this board."};const r=new Set(e.prs.map($)).size;return{link:"github",ok:!0,detail:r?`${M(r,"open PR")}, all in the last check.`:"No open PRs; the last check reached this board."}}o(Ae,"githubLink");function _e(e){return e?e.ok?{link:"repo-fetch",ok:!0,detail:"The hub's last git fetch of this board's repo worked."}:{link:"repo-fetch",ok:!1,detail:`${e.error.replace(/[.\s]*$/,".")} ${e.hint}.`}:null}o(_e,"repoFetchLink");const Te={listening:{ok:!0,detail:"Its session and a watcher are running."},waking:{ok:!0,detail:"Waking: the hub resumed its session, which listens again once it rejoins."},asleep:{ok:!0,detail:"Asleep: its session idled out, and the hub wakes it on the next event or a tap on its page."},deaf:{ok:!1,detail:"Not listening: its session or its watcher isn't running, and the hub can't wake it, so events wait."}},Ne=o(e=>({link:"events",listen:e,...Te[e]}),"eventsLink");function Le(e,t,n){return(n?.last?.failed||[]).includes(e.route)?{link:"wakes",ok:!1,detail:`The last wake (${n.last.due.join(", ")}) couldn't reach this board.`}:{link:"wakes",ok:!0,detail:`Daily at ${t.quiet.end}, when quiet hours end.`}}o(Le,"wakesLink");function Re(e,t,n){const s=[];let r=0;for(const i of e.prs){const a=t?.prs?.[$(i)];if(a?.state!=="OPEN"||a.isDraft)continue;r++;const{wave:l}=e.waves.find(g=>g.slug===i.wave),u=l.items.find(g=>g.id===i.id),h=ce(l,u,`${i.wave}/${i.id}`,n),m=[...new Set([...e.reviewers,...B(l.reviewers)])].filter(g=>!Object.hasOwn(h,g));m.length&&s.push(`#${i.pr} has no link for ${m.join(", ")}`)}return s.length?{link:"slack",ok:!1,detail:`${s.join("; ")}.`}:{link:"slack",ok:!0,detail:r?`Every reviewer has the link to ${M(r,"PR")} in review.`:"No PR is in review."}}o(Re,"slackLink");const De=/\b(?:from|import)\s*['"](\.\.?\/[^'"]+)['"]/g;function D(e=c.join(fe,"hub","hub.mjs")){const t=c.dirname(c.dirname(e)),n=new Map,s=o(a=>{if(n.has(a))return;let l;try{l=p.readFileSync(a)}catch{return}n.set(a,l);for(const u of l.toString("utf8").matchAll(De))s(c.resolve(c.dirname(a),u[1]))},"visit");s(e);const r=[...n.keys()].map(a=>c.relative(t,a)).sort(),i=q.createHash("sha256");for(const a of r)i.update(a).update("\0").update(n.get(c.join(t,a))).update("\0");return{hash:i.digest("hex").slice(0,12),files:r}}o(D,"hubCode");const bt=o(e=>D(c.resolve(e.cwd||e.dir,e.cmd.find(t=>U.test(t)))).hash,"boardCode");function yt(){const e={...D(),pid:process.pid,at:new Date().toISOString()};return k(P,e),e}o(yt,"recordHubCode");const Ie=ae().restartCommand,Ce=o(()=>f(P,null)?.hash??null,"runningHubCode"),Fe=o(()=>({running:Ce(),onDisk:D().hash}),"hubCodeState"),vt=o(({running:e,onDisk:t}=Fe())=>e&&e===t?{link:"hub",ok:!0,detail:`The hub runs the code in its files (${t}).`}:{link:"hub",ok:!1,detail:`The hub runs older code than its files; restart it with ${Ie}`},"hubLink"),b=6e4,Pe={github:o(e=>(e+10)*b,"github"),"repo-fetch":o(e=>(e+10)*b,"repo-fetch"),events:o(()=>2*b,"events"),wakes:o(e=>(e+10)*b,"wakes"),slack:o(e=>(e+10)*b,"slack")};function Me(e,t,{now:n,faults:s}){const r=s===void 0,i=r?d(f(E,{}),e)||{}:s,a={},l=t.map(u=>{if(u.ok)return{...u,state:"ok"};const h=d(i,u.link)||new Date(n).toISOString();a[u.link]=h;const m=Pe[u.link](ee);return{...u,state:n-Date.parse(h)>=m?"broken":"settling",since:h}});if(r&&JSON.stringify(a)!==JSON.stringify(i)){const u=Object.assign(Object.create(null),f(E,{}));Object.keys(a).length?u[e]=a:delete u[e];try{k(E,u)}catch{}}return l}o(Me,"settle");function Be(e,t={}){const n=t.github??f(pe,{}),s=t.settings??Y(),r=[Ae(e,n),_e(d(t.fetches??f(he,{}),e.route)),Ne(t.listen??N(e)),Le(e,s,t.wakes??f(de,{})),Re(e,n,t.topics??ue())].filter(Boolean);return Me(e.route,r,{now:t.now??Date.now(),faults:t.faults})}o(Be,"linksOf");function St(e){const t={running:y(),wakes:f(x,{})};if(t.running)for(const n of T(e))Be(n,{listen:N(n,t)})}o(St,"recordLinkFaults");export{ke as DEAF_AFTER,pe as GITHUB_STATE,Pe as GRACE,P as HUB_CODE,at as INCOMING_FILES,jt as LINKS,he as REPO_FETCH,x as RESUME_STATE,de as WAKE_STATE,C as WAVES,J as appendEvent,ft as boardAt,bt as boardCode,be as buildsLocally,wt as carryCursor,gt as checkListening,kt as handshake,$e as handshakeFile,lt as hubBoard,D as hubCode,Fe as hubCodeState,vt as hubLink,A as isBoardSite,xe as isListening,Be as linksOf,T as listBoards,N as listenState,Ee as oldestUnheard,$ as prKey,ve as prsOf,f as readJSON,j as readManifest,yt as recordHubCode,St as recordLinkFaults,je as reviewerLogins,Ce as runningHubCode,Se as samePath,k as saveJSON,pt as sendToBoards,dt as stopPreviousWatcher,mt as watchedCounts,R as watchedLines,ht as watcherCursor,Oe as watcherRunning,ct as withManifestLock,ut as writeManifest};
@@ -0,0 +1,20 @@
1
+ var g=Object.defineProperty;var a=(t,e)=>g(t,"name",{value:e,configurable:!0});import h from"node:fs";import u from"node:net";import c from"node:path";import{fileURLToPath as y}from"node:url";const f=c.join(c.dirname(y(import.meta.url)),".."),p={other:{status:403,line:"This kickflip belongs to someone else.",hint:"Ask its owner to share it with you.",json:{}},unread:{status:503,line:"Tailscale didn\u2019t answer, so this kickflip can\u2019t tell who you are.",hint:"Open it on the Mac itself, or try again in a moment.",json:{error:"Tailscale didn\u2019t answer, so the hub can\u2019t tell who you are."}}},b=`html, body { margin: 0; height: 100%; }
2
+ body { display: grid; place-content: center; justify-items: center; gap: 10px; padding: 32px 24px; box-sizing: border-box; background: var(--bg); color: var(--text); text-align: center;
3
+ font: 14px/1.45 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif; }
4
+ .rf-mark { display: block; margin-bottom: 8px; width: 70px; height: 60px; object-fit: contain; }
5
+ .rf-line { margin: 0; font-size: 20px; font-weight: 600; line-height: 1.3; text-wrap: balance; }
6
+ .rf-sub { margin: 0; font-size: 14px; color: var(--muted); }`;function w(t){const{line:e,hint:n}=p[t],r=h.readFileSync(c.join(f,"shared","colors.css"),"utf8"),i=h.readFileSync(c.join(f,"board","public","kickflip.png")).toString("base64");return`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
7
+ <title>kickflip</title><style>${r}
8
+ ${b}</style></head><body>
9
+ <img class="rf-mark" src="data:image/png;base64,${i}" width="70" height="60" alt="">
10
+ <p class="rf-line">${e}</p>
11
+ <p class="rf-sub">${n}</p>
12
+ </body></html>`}a(w,"refusalPage");function k(t,e,n){const{status:r,json:i}=p[n],l=/\btext\/html\b/.test(t.headers.accept||"");e.writeHead(r,{"content-type":l?"text/html; charset=utf-8":"application/json","cache-control":"no-store"}),e.end(l?w(n):JSON.stringify(i)),t.resume()}a(k,"refuse");function v(t,e){const{status:n,json:r}=p[e],i=JSON.stringify(r);t.end(`HTTP/1.1 ${n} ${n===403?"Forbidden":"Service Unavailable"}\r
13
+ Content-Type: application/json\r
14
+ Content-Length: ${Buffer.byteLength(i)}\r
15
+ Connection: close\r
16
+ \r
17
+ ${i}`)}a(v,"refuseUpgrade");function H({req:t,socket:e,head:n,port:r,path:i,replace:l=[],extra:m=[]}){const o=u.connect(r,"127.0.0.1",()=>{const d=[`${t.method} ${i} HTTP/${t.httpVersion}`];for(let s=0;s<t.rawHeaders.length;s+=2)l.includes(t.rawHeaders[s].toLowerCase())||d.push(`${t.rawHeaders[s]}: ${t.rawHeaders[s+1]}`);o.write([...d,...m].join(`\r
18
+ `)+`\r
19
+ \r
20
+ `),n&&n.length&&o.write(n),o.pipe(e),e.pipe(o)});return o.on("error",()=>e.destroy()),e.on("error",()=>o.destroy()),e.on("close",()=>o.destroy()),o.on("close",()=>e.destroy()),o}a(H,"pipeUpgrade");export{H as pipeUpgrade,w as refusalPage,k as refuse,v as refuseUpgrade};
package/hub/github.mjs CHANGED
@@ -1,4 +1,4 @@
1
- var te=Object.defineProperty;var S=(e,s)=>te(e,"name",{value:s,configurable:!0});import J from"node:fs";import z from"node:path";import"node:os";import{savedClient as se}from"./linear.mjs";import{checksPassed as ne,fetchPRs as re,markReady as oe,reportOf as ae}from"./pr-report.mjs";import{wakesDue as ie,localDay as ce}from"./schedule.mjs";import{GITHUB_STATE as H,WAKE_STATE as K,WAVES as _,listBoards as W,appendEvent as U,prKey as N,readJSON as V,saveJSON as F,reviewerLogins as de}from"./boards.mjs";import{appendThread as Q,setItem as fe,mergedLine as le}from"../board/store.mjs";import{allLanded as X,leastReady as ue}from"../shared/card-prs.mjs";const B=["ci","review","comments","merged","mergeable","draft"],C=S(e=>e&&e!=="UNKNOWN"?e:null,"known");function ge(e){const r=(Array.isArray(e.events)?e.events:[]).at(-1);return{state:e.state||null,isDraft:!!e.isDraft,reviewDecision:e.reviewDecision||null,mergeable:e.mergeable||null,mergeStateStatus:e.mergeStateStatus||null,head:e.head||null,failing:[...new Set(e.failing||[])].sort(),pending:Number(e.pending)||0,eventId:r?String(r.id):null,eventAt:r?.at||null}}S(ge,"summarize");function me(e,s){const r=new Set;if(!e)return s.state==="MERGED"&&r.add("merged"),s.eventId&&r.add("comments"),B.filter(a=>r.has(a));(s.pending===0&&(e.pending>0||e.head!==s.head)||e.failing.join(`
1
+ var ne=Object.defineProperty;var k=(e,s)=>ne(e,"name",{value:s,configurable:!0});import j from"node:fs";import z from"node:path";import"node:os";import{savedClient as re}from"./linear.mjs";import{checksPassed as H,fetchPRs as ae,markReady as oe,reportOf as ie}from"./pr-report.mjs";import{wakesDue as ce,localDay as de}from"./schedule.mjs";import{GITHUB_STATE as K,WAKE_STATE as W,WAVES as _,listBoards as U,appendEvent as V,prKey as b,readJSON as F,saveJSON as Q,reviewerLogins as fe}from"./boards.mjs";import{appendThread as X,setItem as le,mergedLine as ue,mergeCard as ge}from"../board/store.mjs";import{allLanded as Y,leastReady as me}from"../shared/card-prs.mjs";import{runOwnerChecks as he}from"./owner-checks.mjs";const B=["ci","review","comments","merged","mergeable","draft"],P=k(e=>e&&e!=="UNKNOWN"?e:null,"known");function pe(e){const r=(Array.isArray(e.events)?e.events:[]).at(-1);return{state:e.state||null,isDraft:!!e.isDraft,reviewDecision:e.reviewDecision||null,mergeable:e.mergeable||null,mergeStateStatus:e.mergeStateStatus||null,head:e.head||null,failing:[...new Set(e.failing||[])].sort(),pending:Number(e.pending)||0,eventId:r?String(r.id):null,eventAt:r?.at||null}}k(pe,"summarize");function we(e,s){const r=new Set;if(!e)return s.state==="MERGED"&&r.add("merged"),s.eventId&&r.add("comments"),B.filter(a=>r.has(a));(s.pending===0&&(e.pending>0||e.head!==s.head)||e.failing.join(`
2
2
  `)!==s.failing.join(`
3
- `))&&r.add("ci"),e.reviewDecision!==s.reviewDecision&&r.add("review"),s.eventId&&s.eventId!==e.eventId&&r.add("comments"),s.state==="MERGED"&&e.state!=="MERGED"&&r.add("merged");for(const a of["mergeable","mergeStateStatus"])C(e[a])&&C(s[a])&&e[a]!==s[a]&&r.add("mergeable");return e.isDraft!==s.isDraft&&r.add("draft"),B.filter(a=>r.has(a))}S(me,"changesBetween");function Y(e,s){const r={...s};for(const n of["mergeable","mergeStateStatus"])!C(s[n])&&C(e?.[n])&&(r[n]=e[n]);return r}S(Y,"carry");function he(e,s){const r=z.join(e,"data","checked.json"),n=`${r}.${process.pid}.tmp`;J.mkdirSync(z.dirname(r),{recursive:!0}),J.writeFileSync(n,JSON.stringify(s?{at:new Date().toISOString(),every:s}:{at:new Date().toISOString(),every:null,idle:!0},null,2)+`
4
- `),J.renameSync(n,r)}S(he,"writeChecked");async function Ae({every:e,manifest:s}){const r=new Date().toISOString(),n=V(H,{prs:{}});n.prs||={},n.acknowledged||={},n.threaded||={};const a=W(s),c=[],d=[...new Map(a.flatMap(t=>t.prs).map(t=>[N(t),{repo:t.repo,pr:t.pr}])).values()];let g=null;if(d.length)try{g=await re(d)}catch(t){c.push(t.message)}const v=new Map(d.map((t,u)=>[N(t),g?g.results[u]:null])),f=g?.login??null,i=S((t,u)=>JSON.stringify([N(t),u]),"fetchKey"),m=new Map,E=new Map,w=new Map;for(const t of a){t.me=t.githubLogin||f;for(const u of t.prs){const p=N(u);m.set(p,u);const O=w.get(p)||new Set;if(O.add(t.me),w.set(p,O),!t.me)continue;const A=i(u,t.me),o=E.get(A);(!o||u.since<o.since)&&E.set(A,{...u,me:t.me})}}const l={},$={},h=new Map,D=Object.fromEntries([...m.keys()].map(t=>[t,[]])),k=new Set;for(const[t,u]of E){const p=v.get(N(u));if(!p?.view){l[t]=null,p?.error&&!k.has(p.error)&&(k.add(p.error),c.push(p.error));continue}$[t]=ae(p,u);const O=ge($[t]);l[t]=O,h.has(N(u))||h.set(N(u),O)}const M=[],I=[],L=new Set,Z=new Map,q=new Set,ee=new Set;for(const t of a){const u=t.prs.map(o=>{const y=N(o),R=JSON.stringify([t.route,o.wave,o.id]),b=n.acknowledged[y]?.[R]||n.prs[y],G=l[i(o,t.me)],j=G?me(b,G):[];return j.length&&(D[y]=B.filter(x=>D[y].includes(x)||j.includes(x))),{p:o,key:y,recipient:R,previous:b,next:G,changes:j}});for(const o of u){const y=$[i(o.p,t.me)],R=u.filter(b=>b.p.wave===o.p.wave&&b.p.id===o.p.id);y&&await we(t,o,y,R,{state:n,merged:Z,failed:c})&&q.add(t.route),t.skipDraft&&await ve(t,o,v.get(o.key)?.view,ee,c)}const p=u.filter(o=>o.changes.length);let O=u.every(o=>o.next),A=!0;if(p.length)try{M.push({route:t.route,id:await U(t,"github-change",{prs:p.map(({p:o,changes:y})=>({wave:o.wave,id:o.id,pr:o.pr,changes:y}))})})}catch(o){c.push(`${t.route}: could not add the github-change event (${o.message})`);for(const{key:y}of p)L.add(y);O=A=!1}for(const{key:o,recipient:y,previous:R,next:b,changes:G}of u)b&&(A||!G.length)&&(n.acknowledged[o]||={},n.acknowledged[o][y]=Y(R,b));if(O)try{he(t.dir,t.prs.length?e:null),I.push(t.route)}catch(o){c.push(`${t.route}: could not write checked.json (${o.message})`)}}const P=[];for(const[t,u]of m){const p=h.get(t);!p||L.has(t)||(D[t].length&&P.push({repo:u.repo,pr:u.pr,changes:D[t]}),n.prs[t]={...Y(n.prs[t],p),at:r},w.get(t).size===1&&delete n.acknowledged[t])}for(const t of Object.keys(n.acknowledged))m.has(t)||delete n.acknowledged[t];for(const t of Object.keys(n.threaded))m.has(t)||delete n.threaded[t];n.lastCheck={at:r,watched:m.size,failed:c.length,changed:P.length,boards:I},F(H,n);const T={moved:[],failed:[]};try{await ye(a,l,i,T)}catch(t){T.failed.push(`linear: ${t.message}`)}return{at:r,watched:m.size,failed:c,changes:P,events:M,checked:I,merged:[...q],linear:T,rate:g?.rate??null}}S(Ae,"checkGitHub");function pe(e){const s=(e.body||"").trim();return[e.kind==="review"&&{APPROVED:"Approved.",CHANGES_REQUESTED:"Changes requested."}[e.state],s].filter(Boolean).join(" ")}S(pe,"lineText");async function we(e,{p:s,key:r,recipient:n},a,c,{state:d,merged:g,failed:v}){const f=`${s.wave}/${s.id}`,i=new Set(d.threaded[r]?.[n]||[]),m=e.waves.find(l=>l.slug===s.wave)?.wave,E={...e.reviewerLogins,...de(m?.reviewers)};for(const l of a.events){const $=`${l.kind}:${l.id}`;if(!["user","reviewer"].includes(l.who)||l.at<=s.since||i.has($))continue;const h=pe(l);try{h&&await Q(e.dir,f,{text:h,via:"github",who:l.who==="user"?"love":E[String(l.login).toLowerCase()]||l.login,link:l.url||`https://github.com/${s.repo}/pull/${s.pr}`}),i.add($)}catch(D){v.push(`${e.route}: could not write ${f}'s GitHub line (${D.message})`)}}if(i.size&&((d.threaded[r]||={})[n]=[...i]),a.state!=="MERGED"||!X(c.map(l=>l.next)))return!1;const w=JSON.stringify([e.kind==="waves"?_:e.dir,s.wave,s.id]);if(!g.has(w)){const l=e.github||e.waves.find(h=>h.slug===s.wave)?.wave.repo,$=le(c.filter(h=>h.next.state==="MERGED").map(h=>({repo:h.p.repo,number:h.p.pr})),l);try{await fe(e.dir,f,{status:"merged",force:!0,text:$}),g.set(w,!0)}catch(h){v.push(`${e.route}: could not record ${f} merged (${h.message})`),g.set(w,!1)}}return g.get(w)}S(we,"recordOnCard");async function ve(e,{p:s,key:r},n,a,c){if(!(a.has(r)||n?.state!=="OPEN"||!n.isDraft||!ne(n.statusCheckRollup))){a.add(r);try{await oe({repo:s.repo,pr:s.pr})}catch(d){c.push(`${e.route}: could not mark ${r} ready for review (${d.message})`);return}try{await Q(e.dir,`${s.wave}/${s.id}`,{text:`CI passed, so PR #${s.pr} is marked ready for review.`})}catch(d){c.push(`${e.route}: could not write ${s.wave}/${s.id}'s ready line (${d.message})`)}}}S(ve,"readyGreenDraft");async function ye(e,s,r,n){const a=se();if(!a)return;const c=new Map;for(const d of e){const g=Map.groupBy(d.prs,f=>JSON.stringify([f.wave,f.id])),v=d.kind==="waves"?_:d.dir;for(const{slug:f,wave:i}of d.waves)for(const m of i.items){if(m.status==="dropped")continue;const E=new Set(m.linear||[]);d.tickets?.source==="linear"&&m.ticket&&E.add(m.ticket);const w=(g.get(JSON.stringify([f,m.id]))||[]).map(k=>s[r(k,d.me)]||null),l=w[ue(w)],$=["merged","done"].includes(m.status)||X(w),h={merged:$,ready:l?.state==="OPEN"&&!l.isDraft,hasPR:w.length>0&&($||w.some(k=>k?.state==="OPEN")),failed:w.some(k=>!k)},D=JSON.stringify([v,f,m.id]);for(const k of E){c.has(k)||c.set(k,{items:new Map,scoped:!1,created:!1});const M=c.get(k),I=M.items.get(D);M.items.set(D,{...h,failed:h.failed||I?.failed}),M.scoped||=w.some(Boolean),M.created||=m.ticket===k&&!!m.ticket_created}}}for(const[d,g]of c){const v=[...g.items.values()];if(!g.scoped||v.some(i=>i.failed))continue;const f=v.every(i=>i.merged)?"done":v.every(i=>i.merged||i.ready)?"review":v.some(i=>i.hasPR)?"started":null;if(f)try{const i=await a.advanceIssue(d,f,{owner:a.saved.viewer.id,created:g.created});i&&n.moved.push({id:d,state:i})}catch(i){n.failed.push(`linear ${d}: ${i.message}`)}}}S(ye,"advanceLinear");async function Ge({now:e=new Date,settings:s,manifest:r}){const n=V(K,{});n.sent||={};const a=ie(e,s,n.sent),c=[],d=[],g=[];if(!a.length)return{due:a,events:c,failed:d};const v=W(r);for(const f of v)try{c.push({route:f.route,id:await U(f,"check-github",{reason:"morning"})})}catch(i){d.push(`${f.route}: could not add the morning check-github event (${i.message})`),g.push(f.route)}if(c.length||!v.length){for(const f of a)n.sent[f]=ce(e);n.last={at:e.toISOString(),due:a,routes:c.map(f=>f.route),failed:g},F(K,n)}return{due:a,events:c,failed:d}}S(Ge,"sendWakes");export{B as CHANGE_KINDS,Y as carry,me as changesBetween,Ae as checkGitHub,Ge as sendWakes,ge as summarize};
3
+ `))&&r.add("ci"),e.reviewDecision!==s.reviewDecision&&r.add("review"),s.eventId&&s.eventId!==e.eventId&&r.add("comments"),s.state==="MERGED"&&e.state!=="MERGED"&&r.add("merged");for(const a of["mergeable","mergeStateStatus"])P(e[a])&&P(s[a])&&e[a]!==s[a]&&r.add("mergeable");return e.isDraft!==s.isDraft&&r.add("draft"),B.filter(a=>r.has(a))}k(we,"changesBetween");function Z(e,s){const r={...s};for(const n of["mergeable","mergeStateStatus"])!P(s[n])&&P(e?.[n])&&(r[n]=e[n]);return r}k(Z,"carry");function ye(e,s){const r=z.join(e,"data","checked.json"),n=`${r}.${process.pid}.tmp`;j.mkdirSync(z.dirname(r),{recursive:!0}),j.writeFileSync(n,JSON.stringify(s?{at:new Date().toISOString(),every:s}:{at:new Date().toISOString(),every:null,idle:!0},null,2)+`
4
+ `),j.renameSync(n,r)}k(ye,"writeChecked");async function Je({every:e,manifest:s}){const r=new Date().toISOString(),n=F(K,{prs:{}});n.prs||={},n.acknowledged||={},n.threaded||={};const a=U(s),i=[],c=[...new Map(a.flatMap(t=>t.prs).map(t=>[b(t),{repo:t.repo,pr:t.pr}])).values()];let g=null;if(c.length)try{g=await ae(c)}catch(t){i.push(t.message)}const w=new Map(c.map((t,u)=>[b(t),g?g.results[u]:null])),f=g?.login??null,d=k((t,u)=>JSON.stringify([b(t),u]),"fetchKey"),m=new Map,E=new Map,y=new Map;for(const t of a){t.me=t.githubLogin||f;for(const u of t.prs){const p=b(u);m.set(p,u);const O=y.get(p)||new Set;if(O.add(t.me),y.set(p,O),!t.me)continue;const C=d(u,t.me),o=E.get(C);(!o||u.since<o.since)&&E.set(C,{...u,me:t.me})}}const l={},$={},h=new Map,D=Object.fromEntries([...m.keys()].map(t=>[t,[]])),S=new Set;for(const[t,u]of E){const p=w.get(b(u));if(!p?.view){l[t]=null,p?.error&&!S.has(p.error)&&(S.add(p.error),i.push(p.error));continue}$[t]=ie(p,u);const O=pe($[t]);l[t]=O,h.has(b(u))||h.set(b(u),O)}const M=[],A=[],L=new Set,ee=new Map,q=new Set,te=new Set,se=new Set;for(const t of a){const u=t.prs.map(o=>{const v=b(o),R=JSON.stringify([t.route,o.wave,o.id]),N=n.acknowledged[v]?.[R]||n.prs[v],I=l[d(o,t.me)],J=I?we(N,I):[];return J.length&&(D[v]=B.filter(x=>D[v].includes(x)||J.includes(x))),{p:o,key:v,recipient:R,previous:N,next:I,changes:J}});for(const o of u){const v=$[d(o.p,t.me)],R=u.filter(N=>N.p.wave===o.p.wave&&N.p.id===o.p.id);v&&await ke(t,o,v,R,{state:n,merged:ee,failed:i})&&q.add(t.route),t.skipDraft&&await Se(t,o,w.get(o.key)?.view,te,i),t.autoMerge&&await $e(t,o,w.get(o.key)?.view,se,i)}const p=u.filter(o=>o.changes.length);let O=u.every(o=>o.next),C=!0;if(p.length)try{M.push({route:t.route,id:await V(t,"github-change",{prs:p.map(({p:o,changes:v})=>({wave:o.wave,id:o.id,pr:o.pr,changes:v}))})})}catch(o){i.push(`${t.route}: could not add the github-change event (${o.message})`);for(const{key:v}of p)L.add(v);O=C=!1}for(const{key:o,recipient:v,previous:R,next:N,changes:I}of u)N&&(C||!I.length)&&(n.acknowledged[o]||={},n.acknowledged[o][v]=Z(R,N));if(O)try{ye(t.dir,t.prs.length?e:null),A.push(t.route)}catch(o){i.push(`${t.route}: could not write checked.json (${o.message})`)}}const G=[];for(const[t,u]of m){const p=h.get(t);!p||L.has(t)||(D[t].length&&G.push({repo:u.repo,pr:u.pr,changes:D[t]}),n.prs[t]={...Z(n.prs[t],p),at:r},y.get(t).size===1&&delete n.acknowledged[t])}for(const t of Object.keys(n.acknowledged))m.has(t)||delete n.acknowledged[t];for(const t of Object.keys(n.threaded))m.has(t)||delete n.threaded[t];await he(a,n,i),n.lastCheck={at:r,watched:m.size,failed:i.length,changed:G.length,boards:A},Q(K,n);const T={moved:[],failed:[]};try{await De(a,l,d,T)}catch(t){T.failed.push(`linear: ${t.message}`)}return{at:r,watched:m.size,failed:i,changes:G,events:M,checked:A,merged:[...q],linear:T,rate:g?.rate??null}}k(Je,"checkGitHub");function ve(e){const s=(e.body||"").trim();return[e.kind==="review"&&{APPROVED:"Approved.",CHANGES_REQUESTED:"Changes requested."}[e.state],s].filter(Boolean).join(" ")}k(ve,"lineText");async function ke(e,{p:s,key:r,recipient:n},a,i,{state:c,merged:g,failed:w}){const f=`${s.wave}/${s.id}`,d=new Set(c.threaded[r]?.[n]||[]),m=e.waves.find(l=>l.slug===s.wave)?.wave,E={...e.reviewerLogins,...fe(m?.reviewers)};for(const l of a.events){const $=`${l.kind}:${l.id}`;if(!["user","reviewer"].includes(l.who)||l.at<=s.since||d.has($))continue;const h=ve(l);try{h&&await X(e.dir,f,{text:h,via:"github",who:l.who==="user"?"love":E[String(l.login).toLowerCase()]||l.login,link:l.url||`https://github.com/${s.repo}/pull/${s.pr}`}),d.add($)}catch(D){w.push(`${e.route}: could not write ${f}'s GitHub line (${D.message})`)}}if(d.size&&((c.threaded[r]||={})[n]=[...d]),a.state!=="MERGED"||!Y(i.map(l=>l.next)))return!1;const y=JSON.stringify([e.kind==="waves"?_:e.dir,s.wave,s.id]);if(!g.has(y)){const l=e.github||e.waves.find(h=>h.slug===s.wave)?.wave.repo,$=ue(i.filter(h=>h.next.state==="MERGED").map(h=>({repo:h.p.repo,number:h.p.pr})),l);try{await le(e.dir,f,{status:"merged",force:!0,text:$}),g.set(y,!0)}catch(h){w.push(`${e.route}: could not record ${f} merged (${h.message})`),g.set(y,!1)}}return g.get(y)}k(ke,"recordOnCard");async function Se(e,{p:s,key:r},n,a,i){if(!(a.has(r)||n?.state!=="OPEN"||!n.isDraft||!H(n.statusCheckRollup))){a.add(r);try{await oe({repo:s.repo,pr:s.pr})}catch(c){i.push(`${e.route}: could not mark ${r} ready for review (${c.message})`);return}try{await X(e.dir,`${s.wave}/${s.id}`,{text:`CI passed, so PR #${s.pr} is marked ready for review.`})}catch(c){i.push(`${e.route}: could not write ${s.wave}/${s.id}'s ready line (${c.message})`)}}}k(Se,"readyGreenDraft");async function $e(e,{p:s},r,n,a){const i=`${s.wave}/${s.id}`,c=JSON.stringify([e.dir,i]);if(!(n.has(c)||r?.state!=="OPEN"||r.isDraft||r.reviewDecision!=="APPROVED"||r.mergeStateStatus!=="CLEAN"||!H(r.statusCheckRollup))){n.add(c);try{await ge(e.dir,i)}catch(g){g.message.startsWith("Not merged:")||a.push(`${e.route}: could not merge ${i} (${g.message})`)}}}k($e,"mergeApproved");async function De(e,s,r,n){const a=re();if(!a)return;const i=new Map;for(const c of e){const g=Map.groupBy(c.prs,f=>JSON.stringify([f.wave,f.id])),w=c.kind==="waves"?_:c.dir;for(const{slug:f,wave:d}of c.waves)for(const m of d.items){if(m.status==="dropped")continue;const E=new Set(m.linear||[]);c.tickets?.source==="linear"&&m.ticket&&E.add(m.ticket);const y=(g.get(JSON.stringify([f,m.id]))||[]).map(S=>s[r(S,c.me)]||null),l=y[me(y)],$=["merged","done"].includes(m.status)||Y(y),h={merged:$,ready:l?.state==="OPEN"&&!l.isDraft,hasPR:y.length>0&&($||y.some(S=>S?.state==="OPEN")),failed:y.some(S=>!S)},D=JSON.stringify([w,f,m.id]);for(const S of E){i.has(S)||i.set(S,{items:new Map,scoped:!1,created:!1});const M=i.get(S),A=M.items.get(D);M.items.set(D,{...h,failed:h.failed||A?.failed}),M.scoped||=y.some(Boolean),M.created||=m.ticket===S&&!!m.ticket_created}}}for(const[c,g]of i){const w=[...g.items.values()];if(!g.scoped||w.some(d=>d.failed))continue;const f=w.every(d=>d.merged)?"done":w.every(d=>d.merged||d.ready)?"review":w.some(d=>d.hasPR)?"started":null;if(f)try{const d=await a.advanceIssue(c,f,{owner:a.saved.viewer.id,created:g.created});d&&n.moved.push({id:c,state:d})}catch(d){n.failed.push(`linear ${c}: ${d.message}`)}}}k(De,"advanceLinear");async function je({now:e=new Date,settings:s,manifest:r}){const n=F(W,{});n.sent||={};const a=ce(e,s,n.sent),i=[],c=[],g=[];if(!a.length)return{due:a,events:i,failed:c};const w=U(r);for(const f of w)try{i.push({route:f.route,id:await V(f,"check-github",{reason:"morning"})})}catch(d){c.push(`${f.route}: could not add the morning check-github event (${d.message})`),g.push(f.route)}if(i.length||!w.length){for(const f of a)n.sent[f]=de(e);n.last={at:e.toISOString(),due:a,routes:i.map(f=>f.route),failed:g},Q(W,n)}return{due:a,events:i,failed:c}}k(je,"sendWakes");export{B as CHANGE_KINDS,Z as carry,we as changesBetween,Je as checkGitHub,je as sendWakes,pe as summarize};