@lovek4d/kickflip 0.9.0 → 0.10.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.
- package/SKILL.md +6 -6
- package/bin/event-lines.mjs +1 -1
- package/board/bin/board.mjs +16 -20
- package/board/headless.mjs +3 -3
- package/board/store.mjs +29 -29
- package/hub/github.mjs +1 -1
- package/hub/pr-report.mjs +2 -2
- package/hub/review-send.mjs +1 -1
- package/package.json +1 -1
- package/shared/colors.css +3 -3
package/hub/github.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var le=Object.defineProperty;var v=(e,s)=>le(e,"name",{value:s,configurable:!0});import F from"node:fs";import K from"node:path";import"node:os";import{savedClient as fe}from"./linear.mjs";import{checksPassed as W,fetchPRs as ue,markReady as me,reportOf as pe}from"./pr-report.mjs";import{wakesDue as ge,localDay as he}from"./schedule.mjs";import{GITHUB_STATE as U,WAKE_STATE as V,WAVES as Q,listBoards as X,appendEvent as Y,prKey as P,readJSON as Z,saveJSON as ee,reviewerLogins as we}from"./boards.mjs";import{appendMessage as ye,appendStatus as te,setItem as se,mergeCard as ke}from"../board/store.mjs";import{allLanded as ne,cardPRs as Se,leastReady as $e,prNames as ve}from"../shared/card-prs.mjs";const re=["MERGED","CLOSED"];import{runOwnerChecks as De}from"./owner-checks.mjs";import{audit as b,boardAudit as G,hubAudit as ie}from"../shared/audit.mjs";const H=["ci","review","comments","merged","mergeable","draft","linked"],T=v(e=>e&&e!=="UNKNOWN"?e:null,"known");function Ee(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,title:e.title??null,additions:e.additions??null,deletions:e.deletions??null,files:e.files??null,approved:!!e.approved,
|
|
1
|
+
var le=Object.defineProperty;var v=(e,s)=>le(e,"name",{value:s,configurable:!0});import F from"node:fs";import K from"node:path";import"node:os";import{savedClient as fe}from"./linear.mjs";import{checksPassed as W,fetchPRs as ue,markReady as me,reportOf as pe}from"./pr-report.mjs";import{wakesDue as ge,localDay as he}from"./schedule.mjs";import{GITHUB_STATE as U,WAKE_STATE as V,WAVES as Q,listBoards as X,appendEvent as Y,prKey as P,readJSON as Z,saveJSON as ee,reviewerLogins as we}from"./boards.mjs";import{appendMessage as ye,appendStatus as te,setItem as se,mergeCard as ke}from"../board/store.mjs";import{allLanded as ne,cardPRs as Se,leastReady as $e,prNames as ve}from"../shared/card-prs.mjs";const re=["MERGED","CLOSED"];import{runOwnerChecks as De}from"./owner-checks.mjs";import{audit as b,boardAudit as G,hubAudit as ie}from"../shared/audit.mjs";const H=["ci","review","comments","merged","mergeable","draft","linked"],T=v(e=>e&&e!=="UNKNOWN"?e:null,"known");function Ee(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,title:e.title??null,additions:e.additions??null,deletions:e.deletions??null,files:e.files??null,approved:!!e.approved,url:e.url??null,mergedAt:e.mergedAt??null,headRefName:e.headRefName??null,headRefOid:e.headRefOid??null,updatedAt:e.updatedAt??null,checks:e.checks??null,reviews:e.reviews??{},codexOpen:Number(e.codexOpen)||0}}v(Ee,"summarize");function Oe(e,s){const r=new Set;if(!e)return s.state==="MERGED"&&r.add("merged"),s.eventId&&r.add("comments"),H.filter(i=>r.has(i));(s.pending===0&&(e.pending>0||e.head!==s.head)||e.failing.join(`
|
|
2
2
|
`)!==s.failing.join(`
|
|
3
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 i of["mergeable","mergeStateStatus"])T(e[i])&&T(s[i])&&e[i]!==s[i]&&r.add("mergeable");return e.isDraft!==s.isDraft&&r.add("draft"),H.filter(i=>r.has(i))}v(Oe,"changesBetween");function oe(e,s){const r={...s};for(const n of["mergeable","mergeStateStatus"])!T(s[n])&&T(e?.[n])&&(r[n]=e[n]);return r}v(oe,"carry");function Re(e,s){const r=K.join(e,"data","checked.json"),n=`${r}.${process.pid}.tmp`;F.mkdirSync(K.dirname(r),{recursive:!0}),F.writeFileSync(n,JSON.stringify(s?{at:new Date().toISOString(),every:s}:{at:new Date().toISOString(),every:null,idle:!0},null,2)+`
|
|
4
4
|
`),F.renameSync(n,r)}v(Re,"writeChecked");async function We({every:e,manifest:s}){const r=new Date().toISOString(),n=Z(U,{prs:{}});n.prs||={},n.acknowledged||={},n.threaded||={};const i=X(s),a=[],c=[...new Map(i.flatMap(t=>t.prs).map(t=>[P(t),{repo:t.repo,pr:t.pr}])).values()],f=[...new Set(i.flatMap(Ne))];let o=null;if(c.length||f.length)try{o=await ue(c,f)}catch(t){a.push(t.message)}const d=new Map(i.map(t=>[t.route,[]]));if(o)for(const t of i)d.set(t.route,await Ae(t,o.open,a));const g=new Map(c.map((t,p)=>[P(t),o?o.results[p]:null])),h=o?.login??null,u=v((t,p)=>JSON.stringify([P(t),p]),"fetchKey"),y=new Map,R=new Map,E=new Map;for(const t of i){t.me=t.githubLogin||h;for(const p of t.prs){const k=P(p);y.set(k,p);const N=E.get(k)||new Set;if(N.add(t.me),E.set(k,N),!t.me)continue;const C=u(p,t.me),l=R.get(C);(!l||p.since<l.since)&&R.set(C,{...p,me:t.me})}}const m={},O={},w=new Map,D=Object.fromEntries([...y.keys()].map(t=>[t,[]])),S=new Set;for(const[t,p]of R){const k=g.get(P(p));if(!k?.view){m[t]=null,k?.error&&!S.has(k.error)&&(S.add(k.error),a.push(k.error));continue}O[t]=pe(k,p);const N=Ee(O[t]);m[t]=N,w.has(P(p))||w.set(P(p),N)}const _=[],B=[],q=new Set,ae=new Map,x=new Set,ce=new Set,de=new Set;for(const t of i){const p=t.prs.map(l=>{const $=P(l),A=JSON.stringify([t.route,l.file,l.id]),M=n.acknowledged[$]?.[A]||n.prs[$],I=m[u(l,t.me)],L=I?Oe(M,I):[];return L.length&&(D[$]=H.filter(z=>D[$].includes(z)||L.includes(z))),{p:l,key:$,recipient:A,previous:M,next:I,changes:L}});for(const l of p){const $=O[u(l.p,t.me)],A=p.filter(M=>M.p.file===l.p.file&&M.p.id===l.p.id);$&&await Pe(t,l,$,A,{state:n,merged:ae,failed:a})&&x.add(t.route),t.skipDraft&&await be(t,l,g.get(l.key)?.view,ce,a),t.autoMerge&&await Ce(t,l,g.get(l.key)?.view,de,a)}const k=p.filter(l=>l.changes.length);let N=p.every(l=>l.next),C=!0;if(k.length||d.get(t.route).length)try{const l=[...k.map(({p:$,changes:A})=>({file:$.file,id:$.id,pr:$.pr,changes:A})),...d.get(t.route)];_.push({route:t.route,id:await Y(t,"github-change",{prs:l})})}catch(l){a.push(`${t.route}: could not add the github-change event (${l.message})`);for(const{key:$}of k)q.add($);N=C=!1}for(const{key:l,recipient:$,previous:A,next:M,changes:I}of p)M&&(C||!I.length)&&(n.acknowledged[l]||={},n.acknowledged[l][$]=oe(A,M));if(N)try{Re(t.dir,t.prs.length?e:null),B.push(t.route)}catch(l){a.push(`${t.route}: could not write checked.json (${l.message})`)}}const J=[];for(const[t,p]of y){const k=w.get(t);!k||q.has(t)||(D[t].length&&J.push({repo:p.repo,pr:p.pr,changes:D[t]}),n.prs[t]={...oe(n.prs[t],k),at:r},E.get(t).size===1&&delete n.acknowledged[t])}for(const t of Object.keys(n.acknowledged))y.has(t)||delete n.acknowledged[t];for(const t of Object.keys(n.threaded))y.has(t)||delete n.threaded[t];await De(i,n,a),n.lastCheck={at:r,watched:y.size,failed:a.length,changed:J.length,boards:B},ee(U,n);const j={moved:[],failed:[]};try{await Ie(i,m,u,j)}catch(t){j.failed.push(`linear: ${t.message}`)}return{at:r,watched:y.size,failed:a,changes:J,events:_,checked:B,merged:[...x],linear:j,rate:o?.rate??null}}v(We,"checkGitHub");const Ne=v(e=>e.github?[e.github]:[...new Set(e.files.map(s=>s.itemFile.repo).filter(Boolean))],"repoList");async function Ae(e,s,r){const n=[];for(const{slug:i,itemFile:a}of e.files){const c=e.github||a.repo,f=new Map((s[c]||[]).filter(o=>!o.isCrossRepository).map(o=>[o.headRefName,o.number]));for(const o of a.items){if(!f.has(o.id)||o.lane==="done"||Se(o,c).some(h=>h.repo===c))continue;const d=`${i}/${o.id}`,g=f.get(o.id);try{await se(e.dir,d,{pr:g}),await te(e.dir,d,"pr-found",{pr:g,branch:o.id}),n.push({file:i,id:o.id,pr:g,changes:["linked"]})}catch(h){r.push(`${e.route}: could not link PR #${g} to ${d} (${h.message})`)}}}return n}v(Ae,"linkHandPRs");function Me(e){const s=(e.body||"").trim();return[e.kind==="review"&&{APPROVED:"Approved.",CHANGES_REQUESTED:"Changes requested."}[e.state],s].filter(Boolean).join(" ")}v(Me,"lineText");async function Pe(e,{p:s,key:r,recipient:n},i,a,{state:c,merged:f,failed:o}){const d=`${s.file}/${s.id}`,g=new Set(c.threaded[r]?.[n]||[]),h=e.files.find(m=>m.slug===s.file)?.itemFile,u={...e.reviewerLogins,...we(h?.reviewers)};for(const m of i.events){const O=`${m.kind}:${m.id}`;if(!["user","reviewer"].includes(m.who)||m.at<=s.since||g.has(O))continue;const w=Me(m);try{w&&await ye(e.dir,d,{text:w,via:"github",who:m.who==="user"?"love":u[String(m.login).toLowerCase()]||m.login,link:m.url||`https://github.com/${s.repo}/pull/${s.pr}`}),g.add(O)}catch(D){o.push(`${e.route}: could not write ${d}'s GitHub line (${D.message})`)}}g.size&&((c.threaded[r]||={})[n]=[...g]);const y=a.map(m=>m.next);if(!re.includes(i.state)||!y.every(m=>re.includes(m?.state)))return!1;const R=ne(y),E=JSON.stringify([e.kind==="waves"?Q:e.dir,s.file,s.id]);if(!f.has(E)){const m=e.github||e.files.find(S=>S.slug===s.file)?.itemFile.repo,O=a.filter(S=>S.next.state==="MERGED").map(S=>({repo:S.p.repo,number:S.p.pr})),w=a.map(S=>({repo:S.p.repo,number:S.p.pr})),D=R?void 0:["prs-closed",{prs:ve(w,m)}];try{await se(e.dir,d,{merged:O,force:!0,closing:D}),f.set(E,R)}catch(S){o.push(`${e.route}: could not record ${d} ${R?"merged":"closed"} (${S.message})`),f.set(E,!1)}}return f.get(E)}v(Pe,"recordOnCard");async function be(e,{p:s,key:r},n,i,a){if(i.has(r)||n?.state!=="OPEN"||!n.isDraft||!W(n.statusCheckRollup))return;i.add(r);const c={call:"ready-draft",key:`${s.file}/${s.id}`,reason:`The board skips the draft and PR #${s.pr}'s CI passed.`,inputs:{repo:s.repo,pr:s.pr,isDraft:n.isDraft,checks:"passed"}};try{await me({repo:s.repo,pr:s.pr})}catch(f){b(G(e.dir),{...c,result:"failed",reason:`${c.reason} gh failed: ${f.message}`},{once:!0}),a.push(`${e.route}: could not mark ${r} ready for review (${f.message})`);return}b(G(e.dir),{...c,result:"marked ready"});try{await te(e.dir,`${s.file}/${s.id}`,"ci-passed",{pr:s.pr})}catch(f){a.push(`${e.route}: could not write ${s.file}/${s.id}'s ready line (${f.message})`)}}v(be,"readyGreenDraft");async function Ce(e,{p:s},r,n,i){const a=`${s.file}/${s.id}`,c=JSON.stringify([e.dir,a]);if(n.has(c)||r?.state!=="OPEN"||r.isDraft||r.reviewDecision!=="APPROVED"||r.mergeStateStatus!=="CLEAN"||!W(r.statusCheckRollup))return;n.add(c);const f={call:"auto-merge",key:a,reason:`Auto-merge is on and PR #${s.pr} is approved, green and clean.`,inputs:{repo:s.repo,pr:s.pr,reviewDecision:r.reviewDecision,mergeStateStatus:r.mergeStateStatus,checks:"passed"}};try{await ke(e.dir,a),b(G(e.dir),{...f,result:"merged"})}catch(o){b(G(e.dir),{...f,result:"not merged",reason:`${f.reason} ${o.message}`},{once:!0}),o.message.startsWith("Not merged:")||i.push(`${e.route}: could not merge ${a} (${o.message})`)}}v(Ce,"mergeApproved");async function Ie(e,s,r,n){const i=fe();if(!i)return;const a=new Map;for(const c of e){const f=Map.groupBy(c.prs,d=>JSON.stringify([d.file,d.id])),o=c.kind==="waves"?Q:c.dir;for(const{slug:d,itemFile:g}of c.files)for(const h of g.items){const u=new Set(h.linear||[]);c.tickets?.source==="linear"&&h.ticket&&u.add(h.ticket);const y=(f.get(JSON.stringify([d,h.id]))||[]).map(w=>s[r(w,c.me)]||null),R=y[$e(y)],E=h.lane==="done"||ne(y),m={merged:E,ready:R?.state==="OPEN"&&!R.isDraft,hasPR:y.length>0&&(E||y.some(w=>w?.state==="OPEN")),failed:y.some(w=>!w)},O=JSON.stringify([o,d,h.id]);for(const w of u){a.has(w)||a.set(w,{items:new Map,scoped:!1,created:!1,cards:[]});const D=a.get(w),S=D.items.get(O);D.items.set(O,{...m,failed:m.failed||S?.failed}),D.scoped||=y.some(Boolean),D.created||=h.ticket===w&&!!h.ticket_created,D.cards.push(`${c.route}:${d}/${h.id}`)}}}for(const[c,f]of a){const o=[...f.items.values()];if(!f.scoped||o.some(u=>u.failed))continue;const d=o.every(u=>u.merged)?"done":o.every(u=>u.merged||u.ready)?"review":o.some(u=>u.hasPR)?"started":null;if(!d)continue;const g={done:"Every card on it has merged.",review:"Every card on it has merged or has a PR ready for review.",started:"A card on it has a PR."}[d],h={call:"linear-move",key:c,reason:g,inputs:{target:d,cards:f.cards,items:o}};try{const u=await i.advanceIssue(c,d,{owner:i.saved.viewer.id,created:f.created});u&&(n.moved.push({id:c,state:u}),b(ie(),{...h,result:`moved to ${u}`}))}catch(u){b(ie(),{...h,result:"failed",reason:`${g} ${u.message}`},{once:!0}),n.failed.push(`linear ${c}: ${u.message}`)}}}v(Ie,"advanceLinear");async function Ue({now:e=new Date,settings:s,manifest:r}){const n=Z(V,{});n.sent||={};const i=ge(e,s,n.sent),a=[],c=[],f=[];if(!i.length)return{due:i,events:a,failed:c};const o=X(r);for(const d of o)try{a.push({route:d.route,id:await Y(d,"check-github",{reason:"morning"})})}catch(g){c.push(`${d.route}: could not add the morning check-github event (${g.message})`),f.push(d.route)}if(a.length||!o.length){for(const d of i)n.sent[d]=he(e);n.last={at:e.toISOString(),due:i,routes:a.map(d=>d.route),failed:f},ee(V,n)}return{due:i,events:a,failed:c}}v(Ue,"sendWakes");export{H as CHANGE_KINDS,oe as carry,Oe as changesBetween,We as checkGitHub,Ue as sendWakes,Ee as summarize};
|
package/hub/pr-report.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var _=Object.defineProperty;var r=(e,n)=>_(e,"name",{value:n,configurable:!0});import{gh as f,PRIMARY as O}from"../shared/gh.mjs";const S="state,isDraft,reviewDecision,mergeable,mergeStateStatus,headRefOid,statusCheckRollup",
|
|
1
|
+
var _=Object.defineProperty;var r=(e,n)=>_(e,"name",{value:n,configurable:!0});import{gh as f,PRIMARY as O}from"../shared/gh.mjs";const S="state,isDraft,reviewDecision,mergeable,mergeStateStatus,headRefOid,statusCheckRollup",v="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",g=r((e=process.env)=>({...e,PATH:[e.PATH,v].filter(Boolean).join(":")}),"withPath"),w=r(e=>String(e).trim().split(/\r?\n/)[0].slice(0,200),"firstLine");async function h(e,{env:n,account:s}={}){const a=await f(e,{env:g(n),account:s,timeout:9e4,maxBuffer:16777216});if(a.status!==0)throw Object.assign(new Error(w(a.stderr||`gh exited ${a.status}`)),{limited:a.limited});try{return JSON.parse(a.stdout)}catch(l){throw new Error(w(`unreadable output (${l.message})`))}}r(h,"gh");const C="\u{1F916} ",b="<!-- claude-bridge -->";async function y(e){if(!e)return{env:process.env,account:O};const n=await f(["auth","token","-u",e],{env:g()}),s=n.status===0?n.stdout.trim():"";if(!s)throw new Error(`gh has no ${e} account; run gh auth login`);return{env:{...process.env,GH_TOKEN:s},account:e}}r(y,"asLogin");async function L({repo:e,pr:n,replyTo:s,words:a,login:l}){const i=await y(l),p=s===void 0?`repos/${e}/issues/${n}/comments`:`repos/${e}/pulls/${n}/comments/${s}/replies`;try{return(await h(["api",p,"-f",`body=${C}${a}
|
|
2
2
|
|
|
3
|
-
${b}`],
|
|
3
|
+
${b}`],i)).html_url}catch(u){throw new Error(`${e}#${n}: ${u.message}`)}}r(L,"postToPR");async function U({repo:e,pr:n,head:s,body:a,login:l}){const i=await y(l);try{return(await h(["api",`repos/${e}/pulls/${n}/reviews`,"-f",`body=${a}`,"-f","event=COMMENT","-f",`commit_id=${s}`],i)).html_url}catch(p){throw new Error(`${e}#${n}: ${p.message}`)}}r(U,"postReview");async function k(e){return(await h(["api","--paginate","--slurp",`repos/${e}/collaborators`])).flat().map(n=>n.login).sort((n,s)=>n.localeCompare(s))}r(k,"collaborators");async function q(e){try{return(await h(["api",`users/${encodeURIComponent(e)}`])).login}catch{return null}}r(q,"githubUser");async function F({repo:e,pr:n,since:s}){try{return(await h(["api","--paginate","--slurp",`repos/${e}/pulls/${n}/reviews`])).flat().filter(l=>Date.parse(l.submitted_at)>Date.parse(s)).map(l=>({login:l.user?.login??null,at:l.submitted_at}))}catch(a){throw new Error(`${e}#${n}: ${a.message}`)}}r(F,"reviewsSince");async function M({repo:e,pr:n}){const s=await f(["pr","ready",String(n),"--repo",e],{env:g(),timeout:9e4});if(s.status!==0)throw new Error(w(s.stderr||`gh exited ${s.status}`))}r(M,"markReady");const j=r(e=>e.every(n=>n.__typename==="StatusContext"?n.state==="SUCCESS":n.status==="COMPLETED"&&["SUCCESS","NEUTRAL","SKIPPED"].includes(n.conclusion)),"checksPassed");async function B({repo:e,pr:n,since:s,comments:a=!0}){let l;try{l=await h(["pr","view",String(n),"--repo",e,"--json",S])}catch(t){throw Object.assign(new Error(`${e}#${n}: ${t.message}`),{limited:t.limited})}const i=r(t=>h(["api","--paginate","--slurp",`repos/${e}/${t}`]).then(d=>d.flat()).catch(d=>{throw Object.assign(new Error(`${e}#${n}: ${d.message}`),{limited:d.limited})}),"list"),p=Promise.resolve([]),[u,c,m]=await Promise.all([a?i(`issues/${n}/comments?since=${encodeURIComponent(s)}`):p,a?i(`pulls/${n}/comments?since=${encodeURIComponent(s)}`):p,i(`pulls/${n}/reviews`)]);return{view:l,issueComments:u,reviewComments:c,reviews:m}}r(B,"fetchPR");const $="author { __typename login }",x=`state isDraft reviewDecision mergeable mergeStateStatus headRefOid title additions deletions changedFiles url mergedAt headRefName updatedAt latestReviews(first: 50) { nodes { state ${$} } } commits(last: 1) { nodes { commit { statusCheckRollup { contexts(first: 50) { nodes { __typename ... on CheckRun { name status conclusion } ... on StatusContext { context state } } } } } } } comments(last: 50) { nodes { databaseId ${$} createdAt body url } } reviews(last: 50) { nodes { databaseId ${$} submittedAt body state url } } reviewThreads(last: 50) { nodes { comments(first: 20) { nodes { databaseId ${$} createdAt body url path line replyTo { databaseId } } } } }`;async function A(e){const n=await f(["api","graphql","-f",`query=${e}`],{env:g(),timeout:9e4,maxBuffer:67108864});let s=null;try{s=JSON.parse(n.stdout)}catch{}if(s?.data)return s;throw new Error(`GitHub query: ${w(n.status!==0?n.stderr||`gh exited ${n.status}`:"unreadable output")}`)}r(A,"graphql");const E=r(e=>e?.login?{login:e.__typename==="Bot"?`${e.login}[bot]`:e.login}:null,"user");function D(e){const n=new Set(e.map(s=>s.in_reply_to_id).filter(Boolean));return e.filter(s=>!s.in_reply_to_id&&/codex/i.test(s.user?.login||"")&&!n.has(s.id)).length}r(D,"codexOpen");function P(e){let n=0,s=0;for(const a of e){const l=(a.conclusion||a.state||"").toUpperCase(),i=(a.status||"").toUpperCase();["FAILURE","ERROR","CANCELLED","TIMED_OUT","ACTION_REQUIRED","STARTUP_FAILURE"].includes(l)?n++:(i&&i!=="COMPLETED"||l==="PENDING"||l==="EXPECTED")&&s++}return{total:e.length,fail:n,pending:s,state:n?"fail":s?"pending":e.length?"pass":"none"}}r(P,"checksOf");const I="pullRequests(states: OPEN, first: 100, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { number headRefName isCrossRepository } }";async function H(e,n=[]){const s=e.map(({repo:u,pr:c},m)=>{const[t,d]=u.split("/");return`pr${m}: repository(owner: ${JSON.stringify(t)}, name: ${JSON.stringify(d)}) { pullRequest(number: ${Number(c)}) { ${x} } }`});n.forEach((u,c)=>{const[m,t]=u.split("/");s.push(`open${c}: repository(owner: ${JSON.stringify(m)}, name: ${JSON.stringify(t)}) { ${I} }`)});const{data:a,errors:l=[]}=await A(`query { viewer { login } rateLimit { remaining resetAt } ${s.join(" ")} }`),i=Object.fromEntries(n.flatMap((u,c)=>a[`open${c}`]?.pullRequests?[[u,a[`open${c}`].pullRequests.nodes]]:[])),p=e.map(({repo:u,pr:c},m)=>{const t=a[`pr${m}`]?.pullRequest;if(!t)return{error:`${u}#${c}: ${l.find(o=>o.path?.[0]===`pr${m}`)?.message||"GitHub did not answer for it"}`};const d=r(o=>({id:o.databaseId??null,user:E(o.author),created_at:o.createdAt??null,body:o.body??null,html_url:o.url??null}),"comment"),R=(t.reviewThreads?.nodes??[]).flatMap(o=>o.comments?.nodes??[]).map(o=>({...d(o),path:o.path??null,line:o.line??null,in_reply_to_id:o.replyTo?.databaseId??null}));return{view:{state:t.state,isDraft:t.isDraft,reviewDecision:t.reviewDecision,mergeable:t.mergeable,mergeStateStatus:t.mergeStateStatus,headRefOid:t.headRefOid,statusCheckRollup:t.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes??[],title:t.title??null,additions:t.additions??null,deletions:t.deletions??null,changedFiles:t.changedFiles??null,approved:(t.latestReviews?.nodes??[]).some(o=>o.state==="APPROVED"),url:t.url??null,mergedAt:t.mergedAt??null,headRefName:t.headRefName??null,updatedAt:t.updatedAt??null,reviews:Object.fromEntries((t.latestReviews?.nodes??[]).filter(o=>o.author?.login).map(o=>[o.author.login,o.state])),codexOpen:D(R)},issueComments:(t.comments?.nodes??[]).map(d),reviewComments:R,reviews:(t.reviews?.nodes??[]).map(o=>({id:o.databaseId??null,user:E(o.author),submitted_at:o.submittedAt??null,body:o.body??null,state:o.state??null,html_url:o.url??null}))}});return{login:a.viewer?.login??null,rate:a.rateLimit??null,results:p,open:i}}r(H,"fetchPRs");function J({view:e,issueComments:n,reviewComments:s,reviews:a},{pr:l,since:i,me:p}){const u=r((t,d)=>({kind:d,id:t.id??null,login:t.user?.login??null,at:t.created_at??null,body:t.body??null}),"comment"),c=[...n.map(t=>({...u(t,"issue_comment"),url:t.html_url??null})),...s.map(t=>({...u(t,"review_comment"),path:t.path??null,line:t.line??null,in_reply_to:t.in_reply_to_id??null,url:t.html_url??null})),...a.map(t=>({kind:"review",id:t.id??null,login:t.user?.login??null,at:t.submitted_at??null,body:t.body??null,state:t.state??null,url:t.html_url??null}))].filter(t=>t.at!=null&&t.at>i).map(t=>({...t,who:t.login===p?(t.body??"").includes(b)?"me":"user":/codex/.test(t.login)?"codex":/\[bot\]$|^linear/.test(t.login)?"bot":"reviewer"})).filter(t=>t.who!=="me"&&t.who!=="bot").sort((t,d)=>t.at<d.at?-1:t.at>d.at?1:0),m=e.statusCheckRollup??[];return{pr:Number(l),state:e.state??null,isDraft:e.isDraft??null,reviewDecision:e.reviewDecision??null,mergeable:e.mergeable??null,mergeStateStatus:e.mergeStateStatus??null,head:e.headRefOid?.slice(0,10)??null,failing:m.filter(t=>/FAILURE|ERROR/.test(t.conclusion??t.state??"")).map(t=>t.name??t.context??null),pending:m.filter(t=>(t.status??"COMPLETED")!=="COMPLETED").length,events:c,title:e.title??null,additions:e.additions??null,deletions:e.deletions??null,files:e.changedFiles??null,approved:!!e.approved,url:e.url??null,mergedAt:e.mergedAt??null,headRefName:e.headRefName??null,headRefOid:e.headRefOid??null,updatedAt:e.updatedAt??null,checks:P(m),reviews:e.reviews??{},codexOpen:e.codexOpen??0}}r(J,"reportOf");async function G(e){let n;try{n=await h(["pr","list","--repo",e,"--state","open","--limit","100","--json","number,title,author,url,headRefOid,isDraft,files,reviewRequests"])}catch(s){throw new Error(`${e} open PRs: ${s.message}`)}return n.map(s=>({number:s.number,title:s.title,author:s.author?.login??null,url:s.url,head:s.headRefOid?.slice(0,10)??null,draft:!!s.isDraft,files:(s.files||[]).map(a=>a.path),requested:(s.reviewRequests||[]).map(a=>a.login).filter(Boolean)}))}r(G,"openPRs");async function K(e,n){let s;try{s=await h(["pr","view",String(n),"--repo",e,"--json","state,files"])}catch(a){throw new Error(`${e}#${n}: ${a.message}`)}return{state:s.state??null,files:(s.files||[]).map(a=>a.path)}}r(K,"prFiles");async function Q({env:e}={}){const n=await h(["api","user"],{env:e});if(typeof n?.login!="string"||!n.login)throw new Error("gh api user: missing login");return n.login}r(Q,"githubLogin");export{b as CLAUDE_MARK,C as CLAUDE_PREFIX,S as PR_VIEW_FIELDS,P as checksOf,j as checksPassed,k as collaborators,B as fetchPR,H as fetchPRs,Q as githubLogin,q as githubUser,M as markReady,G as openPRs,U as postReview,L as postToPR,K as prFiles,J as reportOf,F as reviewsSince};
|
package/hub/review-send.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var R=Object.defineProperty;var o=(t,r)=>R(t,"name",{value:r,configurable:!0});import _ from"node:fs";import N from"node:path";import{appendEvent as
|
|
1
|
+
var R=Object.defineProperty;var o=(t,r)=>R(t,"name",{value:r,configurable:!0});import _ from"node:fs";import N from"node:path";import{appendEvent as D,listBoards as H}from"./boards.mjs";import{savedSlack as M,slackStatus as x,refusalUnpushed as j,markRefusalPushed as P,missingScope as k,setHearing as y,hearingUnpushed as T,markHearingPushed as B,slackUserId as I}from"./slack.mjs";import{deliver as $}from"./push.mjs";import{reviewDue as U,postLink as q,recordReaction as A,postDigest as E,openDayThread as W,heardThreads as C,markHeard as F,boardPosts as J,ANSWER_MARK as K}from"../board/store.mjs";const v=o(t=>t.status===400&&t.message==="Slack refused the token.","refusedToken");async function L(t,r,p){const n=await U(t.dir,{now:p});if(n.waiting)return{sent:[],failed:[]};const d=[],c=[],a=o(async(e,s)=>{try{await s(),d.push(e)}catch(w){if(v(w))throw w;c.push(`${t.route}: ${e}: ${w.message}`)}},"attempt");for(const e of(n.digest?.reviewers||[]).filter(s=>s.owed))await a(`digest to ${e.name}`,async()=>{await E(t.dir,n.digest.date,e,{channel:e.sent?.channel||await r.openDM(e.slack_user),header:o(s=>`${t.title} digest: ${s} PR${s===1?"":"s"} pending your review`,"header"),post:o(s=>r.post(s),"post")})});const h=new Map,i=o((e,s)=>(h.has(e)||h.set(e,(async()=>W(t.dir,n.date,e,{channel:await s(),header:`${t.title} reviews`,post:o(w=>r.post(w),"post")}))()),h.get(e)),"dayThread");for(const e of n.links)await a(`link to ${e.reviewer} on ${e.pr}`,async()=>{const s=await i(e.reviewer,()=>r.openDM(e.slack_user));await q(t.dir,e,s,()=>r.post({channel:s.channel,thread_ts:s.ts,text:e.text}))});for(const e of n.reactions)await a(`${e.emoji} for ${e.reviewer} on ${e.pr}`,async()=>{await r.react({channel:e.channel,ts:e.ts,name:e.emoji}),await A(t.dir,e)});return{sent:d,failed:c}}o(L,"sendBoard");const O=o(t=>{try{return(JSON.parse(_.readFileSync(N.join(t.dir,"board.json"),"utf8")).reviewers||[]).length>0}catch{return!1}},"hasReviewers");async function z(t,r){return!j()||!t.some(O)?[]:(await $({title:"Slack refused Hub\u2019s token",body:"Reviewer links, reactions and digests wait until a working token is saved in Hub \u203A Integrations \u203A Slack.",url:"/_hub/settings#slack",tag:"slack-token"},{kind:"fault",immediate:!0,at:r}),P(),["pushed that Slack refused the token"])}o(z,"pushRefusal");async function G(t,r,p){const n=[],d=[],c=I(),a=new Map;for(const i of t)try{for(const e of await C(i.dir,p))a.set(e.channel,[...a.get(e.channel)||[],{board:i,thread:e}])}catch(e){d.push(`${i.route}: reading its day threads: ${e.message}`)}const h=o(async(i,e)=>{try{await e()}catch(s){if(k(s))throw s;d.push(`${i}: ${s.message}`)}},"attempt");for(const[i,e]of a)await h(`hearing DM ${i}`,async()=>{const s=e.map(u=>u.thread.ts).sort((u,f)=>Number(u)-Number(f))[0],w=new Map((await r.history({channel:i,oldest:s})).map(u=>[u.ts,u.latest_reply]));y(!0);for(const{board:u,thread:f}of e){const m=f.heard||f.ts;Number(w.get(f.ts))>Number(m)&&await h(`${u.route}: hearing ${f.reviewer}'s ${f.date} thread`,async()=>{const b=(await r.replies({channel:i,ts:f.ts,oldest:m})).filter(l=>Number(l.ts)>Number(m)),S=await J(u.dir);for(const l of b){if(!l.subtype&&!l.bot_id&&!S.has(l.ts)&&!(l.user===c&&l.text?.startsWith(K))){const g=l.user===c?"owner":"reviewer";await D(u,"slack-question",{reviewer:f.reviewer,channel:i,thread_ts:f.ts,ts:l.ts,from:g,answer:g==="reviewer"||/@kickflip\b/i.test(l.text||"")}),n.push(`${u.route}: slack-question from ${g==="owner"?"the owner":f.reviewer}`)}await F(u.dir,f,l.ts)}})}});return{sent:n,failed:d}}o(G,"hearBoards");async function Q(t,r,p){try{return await G(t,r,p)}catch(n){return k(n)?(y(!1),T()?(await $({title:"Slack can\u2019t read the review threads",body:"Reviewers\u2019 messages to the board wait unheard until Hub\u2019s Slack app has im:history: add it, reinstall the app and paste its new token in Hub \u203A Integrations \u203A Slack. Sends go on.",url:"/_hub/settings#slack",tag:"slack-unheard"},{kind:"fault",at:p}),B(),{sent:["pushed that Slack can't read the review threads"],failed:[]}):{sent:[],failed:[]}):{sent:[],failed:[`hearing the day threads: ${n.message}`]}}}o(Q,"hear");async function ae({boards:t=H(),fetch:r,now:p=new Date}={}){const n=M({fetch:r}),d=[],c=[];if(!n)return{sent:d,failed:c};for(const a of t)try{const h=await L(a,n,p);d.push(...h.sent.map(i=>`${a.route}: ${i}`)),c.push(...h.failed)}catch(h){if(c.push(`${a.route}: ${h.message}`),v(h))break}if(!x().refused)try{const a=await Q(t,n,p);d.push(...a.sent),c.push(...a.failed)}catch(a){c.push(`unheard push: ${a.message}`)}try{d.push(...await z(t,p))}catch(a){c.push(`refused-token push: ${a.message}`)}return{sent:d,failed:c}}o(ae,"sendReviews");export{ae as sendReviews};
|
package/package.json
CHANGED
package/shared/colors.css
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
--color-merged: var(--purple);
|
|
11
11
|
}
|
|
12
12
|
@media (prefers-color-scheme: dark) {
|
|
13
|
-
:root { --bg: #
|
|
14
|
-
--accent: #6fa8e8; --accent-soft: #
|
|
15
|
-
--chip: #
|
|
13
|
+
:root { --bg: #0b0c0d; --panel: #111214; --line: #212226; --text: #e8e9ec; --muted: #8a8f98;
|
|
14
|
+
--accent: #6fa8e8; --accent-soft: #141b26; --on-accent: #0b0b0e; --ok: #3ccf7f; --warn: #e3a33a; --bad: #f0687f; --purple: #ab7df8;
|
|
15
|
+
--chip: #1a1b1e; --band: #141517; --claude: #1f1a12; }
|
|
16
16
|
}
|