@lovek4d/kickflip 0.5.3 → 0.6.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/CONTEXT.md +69 -37
- package/SKILL.md +19 -13
- package/bin/kickflip.mjs +12 -12
- package/bin/wait-events.mjs +2 -2
- package/board/agents.mjs +2 -2
- package/board/arch-review.mjs +4 -4
- package/board/bin/board.mjs +156 -83
- package/board/build-script.mjs +2 -1
- package/board/domain.mjs +9 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +9 -5
- package/board/onboarding.mjs +11 -0
- package/board/prototype-base.mjs +1 -0
- package/board/prototype-page.mjs +1 -0
- package/board/public/app-arch.js +1 -1
- package/board/public/app-cards.js +27 -13
- package/board/public/app-header.js +6 -7
- package/board/public/app-lanes.js +15 -38
- package/board/public/app-needs.js +36 -31
- package/board/public/app-snapshot.js +43 -70
- package/board/public/app-threads.js +38 -46
- package/board/public/app.html +9 -4
- package/board/public/app.js +78 -32
- package/board/public/hub-kit.js +62 -2
- package/board/public/hub.css +2 -0
- package/board/public/new-task.js +8 -6
- package/board/public/progress-review.html +46 -0
- package/board/public/progress-review.js +158 -0
- package/board/public/prototype-kit.js +109 -4
- package/board/public/prototype-switcher.js +123 -29
- package/board/public/prototype.html +57 -3
- package/board/public/router.js +7 -6
- package/board/public/shell.js +64 -24
- package/board/public/sidebar.css +1 -0
- package/board/public/sidebar.js +16 -69
- package/board/public/style.css +108 -57
- package/board/public/ui-cards.js +61 -46
- package/board/public/ui-enums.js +1 -1
- package/board/public/ui-header.js +25 -16
- package/board/public/ui-hub.js +12 -1
- package/board/public/ui-links.js +7 -7
- package/board/public/ui-page-header.js +19 -7
- package/board/public/ui-patch.js +7 -33
- package/board/public/ui-progress-bar.js +28 -0
- package/board/public/ui-questions.js +25 -17
- package/board/public/ui-sidebar.js +25 -23
- package/board/public/ui-snapshot.js +43 -52
- package/board/public/ui-threads.js +57 -28
- package/board/public/ui.js +92 -17
- package/board/public/view-hub-settings.js +12 -58
- package/board/public/view-hub.js +2 -1
- package/board/public/view-notes.js +27 -7
- package/board/public/view-questions.js +90 -68
- package/board/public/view-settings.js +133 -15
- package/board/render-check.mjs +10 -0
- package/board/review.mjs +1 -1
- package/board/server.mjs +19 -19
- package/board/stop.mjs +1 -0
- package/board/store.mjs +27 -18
- package/board/sync.mjs +2 -0
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +22 -21
- package/hooks-plugin/hooks/usage-gate.mjs +20 -3
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/front-door.mjs +1 -1
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/incoming.mjs +2 -2
- package/hub/owner-checks.mjs +1 -1
- package/hub/pr-report.mjs +2 -2
- package/hub/review-send.mjs +1 -1
- package/hub/sessions.mjs +2 -2
- package/hub/share.mjs +1 -1
- package/hub/slack.mjs +2 -2
- package/hub/try.mjs +7 -7
- package/hub/upgrade.mjs +1 -1
- package/hub/usage.mjs +3 -3
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/card-prs.mjs +1 -1
- package/shared/machine-gate.mjs +2 -0
- package/shared/prototype-frame.js +1 -0
- package/shared/usage-gate.mjs +1 -1
- package/board/public/ui-phase-bars.js +0 -17
package/board/sync.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var S=Object.defineProperty;var h=(t,o)=>S(t,"name",{value:o,configurable:!0});import x from"node:crypto";import g from"node:fs/promises";const d={items:"key",owner:"id",ownerTopics:"id"},O=new Set(["version","generatedAt","onHost",...Object.keys(d)]),$=1e3,u=h(t=>x.createHash("sha1").update(JSON.stringify(t)??"").digest("base64url").slice(0,16),"hash");function N(t){const o=new Map;for(const[s,r]of Object.entries(d)){const c=t[s]||[];for(const{changed:a,...m}of c)o.set(`${s}:${m[r]}`,u(m));o.set(`order:${s}`,u(c.map(a=>a[r])))}for(const[s,r]of Object.entries(t))O.has(s)||o.set(`part:${s}`,u(r));return o}h(N,"hashes");const b=h(t=>({n:t,floor:t,stamps:{},hashes:{},tombstones:[]}),"freshSync");function v(t,o){const s=N(o),r=[...s].filter(([n,e])=>t.hashes[n]!==e),c=Object.keys(t.hashes).filter(n=>!s.has(n));let a=t;if(r.length||c.length){const n=t.n+1,e={...t.stamps},i={...t.hashes};for(const[p,w]of r)e[p]=n,i[p]=w;for(const p of c)delete e[p],delete i[p];const l=[...t.tombstones.filter(p=>!s.has(p.id)),...c.map(p=>({id:p,n}))],f=l.splice(0,Math.max(0,l.length-$));a={n,floor:Math.max(t.floor,f.at(-1)?.n??t.floor),stamps:e,hashes:i,tombstones:l}}const m={...o,version:a.n};for(const[n,e]of Object.entries(d))o[n]&&(m[n]=o[n].map(i=>({...i,changed:a.stamps[`${n}:${i[e]}`]})));return{sync:a,board:m}}h(v,"stamp");function E(t,o,s){if(!Number.isInteger(s)||s<t.floor||s>t.n)return null;const r=h(e=>t.stamps[e]>s,"after"),c=t.tombstones.filter(e=>e.n>s).map(e=>e.id),a={};for(const[e,i]of Object.entries(o))!O.has(e)&&r(`part:${e}`)&&(a[e]=i);const m=c.filter(e=>e.startsWith("part:")).map(e=>e.slice(5)),n={};for(const[e,i]of Object.entries(d)){const l={set:(o[e]||[]).filter(f=>f.changed>s),drop:c.filter(f=>f.startsWith(`${e}:`)).map(f=>f.slice(e.length+1)),...r(`order:${e}`)?{order:(o[e]||[]).map(f=>f[i])}:{}};(l.set.length||l.drop.length||l.order)&&(n[e]=l)}return{since:s,version:t.n,generatedAt:o.generatedAt,set:a,unset:m,lists:n}}h(E,"patchSince");async function M(t){try{return JSON.parse(await g.readFile(t,"utf8"))}catch(o){if(o.code!=="ENOENT")throw o;return b(Date.now())}}h(M,"readSync");async function A(t,o){await g.writeFile(t+".tmp",JSON.stringify(o)+`
|
|
2
|
+
`),await g.rename(t+".tmp",t)}h(A,"writeSync");export{d as LISTS,$ as TOMBSTONES_KEPT,b as freshSync,E as patchSince,M as readSync,v as stamp,A as writeSync};
|
package/board/tickets.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var L=Object.defineProperty;var o=(e,t)=>L(e,"name",{value:t,configurable:!0});import{runRepo as $,validateGithub as O}from"../hub/repos.mjs";import{savedClient as k}from"../hub/linear.mjs";const i=o(e=>{throw Object.assign(new Error(e),{status:400})},"refuse");function N(e){(!e||typeof e!="object"||Array.isArray(e))&&i("Tickets must name a source.");const t={internal:["source","prefix"],github:["source","account"],linear:["source","workspace","team","project"]};if(Object.hasOwn(t,e.source)||i("Ticket source must be internal, github or linear."),Object.keys(e).some(n=>!t[e.source].includes(n))&&i("Unknown ticket source field."),e.source==="internal"){const n=e.prefix===void 0||e.prefix===""?"INT":e.prefix;return(typeof n!="string"||!/^[A-Z]{2,6}$/.test(n))&&i("Prefix must be 2\u20136 uppercase letters."),{source:"internal",prefix:n}}return e.source==="github"?e.account===void 0||e.account===""?{source:"github"}:((typeof e.account!="string"||!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e.account))&&i("The gh account must be a GitHub login."),{source:"github",account:e.account}):((typeof e.workspace!="string"||!/^[a-z\d][a-z\d-]*$/i.test(e.workspace))&&i("Linear needs a workspace."),(typeof e.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(e.team))&&i("Linear needs an uppercase team key."),(typeof e.project!="string"||!e.project.trim())&&i("Linear needs a project."),{source:"linear",workspace:e.workspace,team:e.team,project:e.project.trim()})}o(N,"ticketSource");const w=o(e=>{const t=e?Number(e[1]):null;return Number.isSafeInteger(t)?t:null},"numberOf"),y=o(e=>w(/^#?([1-9]\d*)$/.exec(String(e??""))),"issueNumber"),x=o(e=>e.source==="internal"?e.prefix||"INT":e.source==="linear"?e.team:null,"keyOf");function g(e,t){if(!e)return null;const n=x(e),r=String(t??"");return n?r.startsWith(`${n}-`)?w(/^([1-9]\d*)$/.exec(r.slice(n.length+1))):null:w(/^#([1-9]\d*)$/.exec(r))}o(g,"ticketIn");function C(e,t,{repo:n,key:r}={}){const
|
|
2
|
-
`)[0]);let f;if(s){let c;try{c=JSON.parse(m.stdout)}catch{i("GitHub returned an invalid issue.")}c.number!==s&&i("GitHub returned a different issue."),f=c.url}else f=m.stdout?.trim();const d=/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/([1-9]\d*)$/.exec(f||"");return(!d||d[1].toLowerCase()!==
|
|
3
|
-
`)[0]})`,"why");try{if(e.source==="linear"){const f=k({fetch:l});if(!f)return`ticket ${
|
|
1
|
+
var L=Object.defineProperty;var o=(e,t)=>L(e,"name",{value:t,configurable:!0});import{runRepo as $,validateGithub as O}from"../hub/repos.mjs";import{savedClient as k}from"../hub/linear.mjs";const i=o(e=>{throw Object.assign(new Error(e),{status:400})},"refuse");function N(e){(!e||typeof e!="object"||Array.isArray(e))&&i("Tickets must name a source.");const t={internal:["source","prefix"],github:["source","account"],linear:["source","workspace","team","project"]};if(Object.hasOwn(t,e.source)||i("Ticket source must be internal, github or linear."),Object.keys(e).some(n=>!t[e.source].includes(n))&&i("Unknown ticket source field."),e.source==="internal"){const n=e.prefix===void 0||e.prefix===""?"INT":e.prefix;return(typeof n!="string"||!/^[A-Z]{2,6}$/.test(n))&&i("Prefix must be 2\u20136 uppercase letters."),{source:"internal",prefix:n}}return e.source==="github"?e.account===void 0||e.account===""?{source:"github"}:((typeof e.account!="string"||!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(e.account))&&i("The gh account must be a GitHub login."),{source:"github",account:e.account}):((typeof e.workspace!="string"||!/^[a-z\d][a-z\d-]*$/i.test(e.workspace))&&i("Linear needs a workspace."),(typeof e.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(e.team))&&i("Linear needs an uppercase team key."),(typeof e.project!="string"||!e.project.trim())&&i("Linear needs a project."),{source:"linear",workspace:e.workspace,team:e.team,project:e.project.trim()})}o(N,"ticketSource");const w=o(e=>{const t=e?Number(e[1]):null;return Number.isSafeInteger(t)?t:null},"numberOf"),y=o(e=>w(/^#?([1-9]\d*)$/.exec(String(e??""))),"issueNumber"),x=o(e=>e.source==="internal"?e.prefix||"INT":e.source==="linear"?e.team:null,"keyOf");function g(e,t){if(!e)return null;const n=x(e),r=String(t??"");return n?r.startsWith(`${n}-`)?w(/^([1-9]\d*)$/.exec(r.slice(n.length+1))):null:w(/^#([1-9]\d*)$/.exec(r))}o(g,"ticketIn");function C(e,t,{repo:n,key:r}={}){const u=g(e,t);if(!u)return null;if(e.source==="internal")return{id:`${x(e)}-${u}`,url:`#item=${encodeURIComponent(r)}`,source:"internal"};if(e.source==="github")return n?{id:`#${u}`,url:`https://github.com/${n}/issues/${u}`,source:"github"}:null;const l=`${e.team}-${u}`;return{id:l,url:`https://linear.app/${e.workspace}/issue/${l}`,source:"linear"}}o(C,"ticketBadge");async function j(e,t){if(!e.account)return t;const n=await t("gh",["auth","token","-u",e.account]);(n.status!==0||!n.stdout?.trim())&&i(`gh has no ${e.account} account; run gh auth login`);const r={...process.env,GH_TOKEN:n.stdout.trim()};return(u,l)=>t(u,l,{env:r})}o(j,"issueRunner");const T=o(async e=>{try{return await e()}catch(t){i(t.message)}},"linearCall");async function E(e,{ticket:t,title:n,body:r="",repo:u,next:l},{run:a=$,linearFetch:b}={}){if(e.source==="internal")return t!==void 0&&!g(e,t)&&i(`Ticket must match ${e.prefix}-<number>.`),{ticket:t??`${e.prefix}-${l}`,created:t===void 0};if(e.source==="linear"){t!==void 0&&!g(e,t)&&i(`Ticket must match ${e.team}-<number>.`);const c=k({fetch:b});if(c||i("Hub has no Linear key, so this board can\u2019t create Linear tickets; add it in Hub settings \u203A Linear, or pick another ticket source."),t===void 0)return{ticket:(await T(()=>c.createIssue({team:e.team,project:e.project,title:n,description:r}))).identifier,created:!0};const h=await T(()=>c.issue(t));return h||i(`Linear has no issue ${t}.`),h.team!==e.team&&i(`${t} is in Linear's ${h.team} team, not ${e.team}.`),{ticket:h.identifier,created:!1}}O(u);const s=t===void 0?null:y(t);t!==void 0&&!s&&i("GitHub ticket must be an issue number such as #41.");const p=s?["issue","view",String(s),"--repo",u,"--json","number,url"]:["issue","create","--repo",u,"--title",n,"--body",r],m=await(await j(e,a))("gh",p);m.status!==0&&i((m.stderr||"GitHub refused the issue.").trim().split(`
|
|
2
|
+
`)[0]);let f;if(s){let c;try{c=JSON.parse(m.stdout)}catch{i("GitHub returned an invalid issue.")}c.number!==s&&i("GitHub returned a different issue."),f=c.url}else f=m.stdout?.trim();const d=/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/([1-9]\d*)$/.exec(f||"");return(!d||d[1].toLowerCase()!==u.toLowerCase()||s&&Number(d[2])!==s||!y(d[2]))&&i("Ticket must be an issue in the board\u2019s repo."),{ticket:`#${d[2]}`,created:!s}}o(E,"resolveTicket");const Z=o(e=>e.startsWith("#")?`GH-${e.slice(1)}`:e,"cardIdOf");function R(e,t){const n=e.source==="github"?String(t??"").match(/(?<![\w#])#[1-9]\d*\b/g):String(t??"").match(/\b[A-Z][A-Z0-9]*-[1-9]\d*\b/g);return[...new Set((n||[]).filter(r=>g(e,r)))]}o(R,"titleTickets");function z({start:e,done:t,handover:n}){return t?"done":e?"started":n?"review":null}o(z,"ticketTarget");const S={started:"In Progress",review:"In Review",done:"Done"};async function _(e,t,n,{repo:r,run:u=$,linearFetch:l}={}){const a=t.ticket;if(!e||!n||!a||!g(e,a)||e.source==="internal"||e.source==="github"&&n!=="done")return null;if(e.source==="github"&&!r)return`ticket ${a}: not closed, the board has no GitHub repo`;const b=o(s=>`ticket ${a}: could not move to ${S[n]} (${String(s.message||s).split(`
|
|
3
|
+
`)[0]})`,"why");try{if(e.source==="linear"){const f=k({fetch:l});if(!f)return`ticket ${a}: not moved, Hub has no Linear key`;if(!t.ticket_created){const c=await f.issue(a);if(!c)return`ticket ${a}: not moved, Linear has no such issue`;if(c.assignee!==f.saved.viewer?.id)return`ticket ${a}: left as it is, not assigned to you`}const d=await f.moveIssue(a,n);return d?`ticket ${a}: moved to ${d}`:null}const s=a.slice(1),p=await j(e,u);if(!t.ticket_created){const f=await p("gh",["issue","view",s,"--repo",r,"--json","state,assignees"]);if(f.status!==0)throw new Error(f.stderr||"gh issue view failed");const d=await p("gh",["api","user","--jq",".login"]);if(d.status!==0)throw new Error(d.stderr||"gh api user failed");const c=JSON.parse(f.stdout);if(c.state!=="OPEN")return null;if(!(c.assignees||[]).some(h=>h.login?.toLowerCase()===d.stdout.trim().toLowerCase()))return`ticket ${a}: left as it is, not assigned to you`}const m=await p("gh",["issue","close",s,"--repo",r]);if(m.status!==0)throw new Error(m.stderr||"gh issue close failed");return`ticket ${a}: closed`}catch(s){return b(s)}}o(_,"moveTicket");export{Z as cardIdOf,_ as moveTicket,E as resolveTicket,C as ticketBadge,g as ticketIn,N as ticketSource,z as ticketTarget,R as titleTickets};
|
package/board/worktree.mjs
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)[0]}}
|
|
1
|
+
var G=Object.defineProperty;var s=(t,e)=>G(t,"name",{value:e,configurable:!0});import d from"node:fs";import p from"node:path";import T from"node:os";import{randomUUID as Q}from"node:crypto";import{spawn as X,spawnSync as q}from"node:child_process";import{fileURLToPath as z}from"node:url";import{BoardError as V,opensPRs as Z,settingsOf as tt}from"./store.mjs";import{boardAt as et,hubCode as nt,listBoards as rt,readJSON as ot,readManifest as it,samePath as st}from"../hub/boards.mjs";import{boardRepo as at}from"../hub/board-repo.mjs";import{cloneRoot as ct}from"../shared/repos.mjs";import{storedPath as j}from"../bin/install.mjs";import{hubService as lt}from"../shared/service.mjs";const ft=p.join(p.dirname(z(import.meta.url)),"..","bin","kickflip.mjs"),C=ct,O=s(()=>p.join(T.homedir(),".claude","kickflip","worktrees"),"worktreesDir"),m=s(t=>{throw new V(t)},"fail");function c(t,...e){const n=q("git",["-C",t,"-c","core.commentChar=;",...e],{encoding:"utf8"});return{ok:n.status===0,out:(n.stdout||"").trim(),err:(n.stderr||n.error?.message||"").trim().split(`
|
|
2
|
+
`)[0]}}s(c,"git");const L=s((t,e)=>c(t,"rev-parse","--short",e).out,"short");function W(t,{repo:e=C(),base:n="main",remote:r=!1,busy:o=[]}={}){const i=p.resolve(e);c(i,"rev-parse","--git-dir").ok||m(`--repo ${i} is not a git repo`),c(i,"check-ref-format","--branch",t).ok||m(`--worktree: "${t}" can't be a branch name`),c(i,"check-ref-format","--branch",n).ok||m("--worktree: invalid base branch");const a=r?`refs/remotes/origin/${n}`:`refs/heads/${n}`;r&&c(i,"fetch","--quiet","--no-tags","origin",`+refs/heads/${n}:${a}`),c(i,"rev-parse","--verify","--quiet",a).ok||m(`--worktree: ${i} has no ${n} branch`);const u=p.join(O(),t),$=d.existsSync(u)?dt(i,u,t,{ref:a,base:n,busy:o}):null;c(i,"rev-parse","--verify","--quiet",`refs/heads/${t}`).ok&&m(`--worktree: branch ${t} already exists in ${i}`),d.mkdirSync(p.dirname(u),{recursive:!0});const h=c(i,"worktree","add","--quiet","-b",t,u,a);return h.ok||m(`--worktree: git worktree add failed: ${h.err}`),{path:j(u),branch:t,repo:j(i),remade:$}}s(W,"makeWorktree");function dt(t,e,n,{ref:r,base:o,busy:i}){const a=s(l=>m(`--worktree: ${e} ${l}; land or remove it first`),"refuse");i.includes(j(e))&&a("already exists and another open builder record uses it");const u=d.realpathSync(e);c(t,"worktree","list","--porcelain").out.split(`
|
|
3
3
|
|
|
4
|
-
`).some(
|
|
5
|
-
`),
|
|
6
|
-
`).filter(Boolean).length,
|
|
7
|
-
`).filter(
|
|
4
|
+
`).some(l=>{const f=l.split(`
|
|
5
|
+
`),v=f.find(b=>b.startsWith("worktree "))?.slice(9);try{return f.includes(`branch refs/heads/${n}`)&&d.realpathSync(v)===u}catch{return!1}})||a("already exists");const h=Number(c(t,"rev-list","--count",`${r}..refs/heads/${n}`).out),k=c(e,"status","--porcelain").out.split(`
|
|
6
|
+
`).filter(Boolean).length,g=s((l,f)=>`${l} ${f}${l===1?"":"s"}`,"count"),w=[...h?[`${g(h,"commit")} beyond ${o}`]:[],...k?[g(k,"changed file")]:[]];w.length&&a(`holds ${w.join(" and ")}`);const S=c(t,"worktree","remove","--force",e);S.ok||m(`--worktree: could not remove the leftover ${e}: ${S.err}`);const y=c(t,"branch","--quiet","-D",n);return y.ok||m(`--worktree: could not delete the leftover branch ${n}: ${y.err}`),`Remade the leftover worktree ${j(e)} from ${o}: it held no commits or changes.`}s(dt,"removeLeftover");function P(t,{repo:e,manifest:n=it()}={}){const r=t?et(t,n):null;if(!r)return{repo:e??C(),base:"main",remote:!1};const o=at(r.route,n),i=e??(o.repo||m(`/${r.route} has no folder recorded for its session; rejoin it from its repo folder`)),a=Z(o,tt(ot(p.join(r.dir,"board.json"),{}),o));return{repo:i,base:o.branch||"main",remote:!o.local||a}}s(P,"builderRepo");const Pt=s((t,e,{repo:n,busy:r}={})=>W(e,{...P(t,{repo:n}),busy:r}),"boardWorktree"),At=s((t,e,{repo:n,base:r,busy:o})=>W(`${t}-${e}`,{repo:n,base:r,busy:o}),"helperWorktree");function Jt(t,e){const n=[];if(c(t.path,"merge-base","--is-ancestor",e.branch,"HEAD").ok)n.push(`${e.branch} is already merged`);else{const o=c(t.path,"merge","--no-edit",e.branch);if(!o.ok){const i=c(t.path,"diff","--name-only","--diff-filter=U").out.split(`
|
|
7
|
+
`).filter(Boolean);m(i.length?`merging ${e.branch} conflicts in ${i.join(", ")}: resolve them, commit, and run agent merge again`:`could not merge ${e.branch}: ${o.err}`)}n.push(`merged ${e.branch} into ${c(t.path,"rev-parse","--abbrev-ref","HEAD").out}`)}if(d.existsSync(e.path)){const o=c(t.path,"worktree","remove",e.path);o.ok||m(`merged, but could not remove ${e.path}: ${o.err}`),n.push(`worktree removed: ${e.path}`)}return c(t.path,"branch","--quiet","-d",e.branch).ok&&n.push(`branch deleted: ${e.branch}`),{lines:n}}s(Jt,"mergeHelper");function Kt(t,e,n){const{repo:r}=P(t),o=p.resolve(r);c(o,"check-ref-format","--branch",n).ok||m(`"${n}" can't be a branch name`);const i=`refs/remotes/origin/${n}`,a=c(o,"fetch","--quiet","--no-tags","origin",`+refs/heads/${n}:${i}`);a.ok||m(`could not fetch ${n} from origin: ${a.err}`);const u=p.join(O(),e);d.existsSync(u)&&m(`${j(u)} already exists; land or remove it first`),d.mkdirSync(p.dirname(u),{recursive:!0});const h=c(o,"rev-parse","--verify","--quiet",`refs/heads/${n}`).ok?c(o,"worktree","add","--quiet",u,n):c(o,"worktree","add","--quiet","--track","-b",n,u,i);return h.ok||m(`git worktree add failed: ${h.err}`),{path:j(u),branch:n,repo:j(o),remade:null}}s(Kt,"prWorktree");function ut(t){const e=[],n=s(r=>{for(const o of d.readdirSync(r,{withFileTypes:!0}))o.name===".git"||o.name===".repos"||o.name==="node_modules"||(o.isDirectory()?n(p.join(r,o.name)):o.name.endsWith(".test.mjs")&&e.push(p.join(r,o.name)))},"walk");return n(t),e.sort()}s(ut,"testFiles");function A(t,{limit:e=900*1e3,testLimit:n=300*1e3}={}){const r=ut(t);if(!r.length)return Promise.resolve({ok:!0,pass:0,fail:0});const{NODE_TEST_CONTEXT:o,...i}=process.env,a=X(process.execPath,["--test","--test-reporter=tap",`--test-timeout=${n}`,...r],{cwd:t,env:i,detached:!0});a.once("exit",()=>{try{process.kill(-a.pid,"SIGKILL")}catch(g){if(g.code!=="ESRCH"&&g.code!=="EPERM")throw g}});let u="",$="";a.stdout.setEncoding("utf8").on("data",g=>{u+=g}),a.stderr.setEncoding("utf8").on("data",g=>{$+=g});let h=null;const k=setTimeout(()=>{h=q("ps",["-A","-o","pgid=,pid=,args="],{encoding:"utf8"}).stdout.split(`
|
|
8
|
+
`).filter(w=>Number(w.trim().split(/\s+/)[0])===a.pid).map(w=>w.trim()),h.length&&process.kill(-a.pid,"SIGKILL");const g=s(()=>{a.stdout.destroy(),a.stderr.destroy()},"release");a.exitCode===null&&a.signalCode===null?a.once("exit",g):g()},e);return new Promise(g=>a.on("close",(w,S)=>{clearTimeout(k);const y=s(x=>Number(u.match(new RegExp(`^# ${x} (\\d+)$`,"m"))?.[1]??0),"count"),l={ok:w===0,pass:y("pass"),fail:y("fail")};if(l.ok){g(l);return}const f=p.join(T.homedir(),".claude","kickflip","logs");try{d.mkdirSync(f,{recursive:!0});const x=p.join(f,`land-${Q()}.tap`);d.writeFileSync(x,u+$+(h?`
|
|
8
9
|
# processes still running at the cutoff
|
|
9
10
|
${h.join(`
|
|
10
11
|
`)}
|
|
11
|
-
`:""),{mode:384}),
|
|
12
|
-
`)[0];
|
|
13
|
-
`),
|
|
14
|
-
`));if(
|
|
15
|
-
${
|
|
16
|
-
`+
|
|
17
|
-
`):""}`)}return
|
|
18
|
-
`)[0]}
|
|
19
|
-
`).find(i=>/^docs\/adr\/\d{4}-(?!\d{2}-\d{2}-\d{4}-)/.test(i));o&&
|
|
20
|
-
`).filter(Boolean);for(const i of o){const
|
|
21
|
-
`)),o.ok||
|
|
22
|
-
`)),
|
|
23
|
-
`).filter(Boolean);
|
|
24
|
-
`).filter(h=>h&&!h.includes(" "));
|
|
25
|
-
`).filter(Boolean),k=
|
|
12
|
+
`:""),{mode:384}),l.log=x}catch(x){l.logError=`Could not save the full test output: ${x.message}`}const v=h?.filter(x=>Number(x.split(/\s+/)[1])!==a.pid);h&&(l.hung={after:mt(e),files:r.filter(x=>v.some(Y=>Y.includes(x))).map(x=>p.relative(t,x))}),l.failures=ht(u);const b=$.trim().split(`
|
|
13
|
+
`)[0];l.error=`${S?`test runner killed by ${S}`:`test runner exited ${w}`}${b?`: ${b}`:""}`,g(l)}))}s(A,"nodeTests");const mt=s(t=>t%6e4?`${t/1e3} seconds`:`${t/6e4} minutes`,"duration");function ht(t){const e=t.split(`
|
|
14
|
+
`),n=[],r=[],o=s(i=>i.startsWith("'")?i.slice(1,-1).replaceAll("''","'"):i.startsWith('"')?JSON.parse(i):i,"scalar");for(let i=0;i<e.length;i++){const a=/^(\s*)# Subtest: (.*)$/.exec(e[i]);if(a){const l=a[1].length;for(;n.length&&n.at(-1).indent>=l;)n.pop();n.push({indent:l,name:a[2]})}const u=/^(\s*)not ok \d+ - (.*)$/.exec(e[i]);if(!u||/ # (?:TODO|SKIP)\b/i.test(u[2]))continue;const $=u[1].length,h=[...n.filter(l=>l.indent<$).map(l=>l.name),u[2]].join(" > "),k=[];for(let l=i+1;l<e.length&&e[l].startsWith(" ".repeat($+2));l++)k.push(e[l]);const g=s(l=>k.find(f=>f.startsWith(`${" ".repeat($+2)}${l}: `))?.slice($+l.length+4),"field"),w=g("location"),S=g("error");let y=[];if(S&&/^[|>]/.test(S)){const l=k.findIndex(f=>f.startsWith(`${" ".repeat($+2)}error: `))+1;for(let f=l;f<k.length&&k[f].startsWith(" ".repeat($+4));f++)y.push(k[f].slice($+4))}else S&&(y=o(S).split(`
|
|
15
|
+
`));if(y[0]==="test failed"){const l=[];let f=i-1;for(e[f]?.startsWith("# Subtest: ")&&f--;f>=0&&e[f].startsWith("# ")&&!e[f].startsWith("# Subtest: ");f--)l.unshift(e[f].slice(2));const v=l.findIndex(b=>/^\w*Error(?: \[[^\]]+\])?:/.test(b));v>=0&&(y=l.slice(v))}r.push(`${h}
|
|
16
|
+
${w?o(w):"file not reported by test runner"}${y.length?`
|
|
17
|
+
`+y.slice(0,3).map(l=>` ${l}`).join(`
|
|
18
|
+
`):""}`)}return r}s(ht,"tapFailures");function pt(t,e){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(o=>typeof o=="string"))&&m("KICKFLIP_RESTART_CMD must be a JSON array of strings"),n=[...n,t,...e?[e]:[]]}else{if(t==="hub")return lt().restart();n=[process.execPath,ft,"restart",e]}const r=q(n[0],n.slice(1),{encoding:"utf8"});return r.status===0?null:(r.stderr||r.error?.message||`exit ${r.status}`).trim().split(`
|
|
19
|
+
`)[0]}s(pt,"restartRunner");const N=s(()=>p.join(T.homedir(),".claude","kickflip"),"kickflipDir"),D=s(()=>p.join(N(),"landing"),"landing"),E=s(t=>{try{return process.kill(t,0),!0}catch(e){return e.code==="EPERM"}},"alive"),$t=5e3;async function J(t){const e=[];let n;try{for(const r of t)try{e.push({file:r,ino:d.statSync(r).ino,watcher:d.watch(r)})}catch{return}await new Promise(r=>{for(const{watcher:o}of e)o.on("change",r).on("error",r);n=setTimeout(r,$t),setImmediate(()=>setImmediate(()=>{for(const{file:o,ino:i}of e){let a=null;try{a=d.statSync(o).ino}catch{}a!==i&&r()}}))})}finally{clearTimeout(n);for(const{watcher:r}of e)r.close()}}s(J,"untilGone");const I=s(t=>p.join(N(),`${t}.queue`),"queueDir"),K=s(t=>Number(t.split("-")[2]),"ticketPid"),gt=s((t,e)=>{try{return JSON.parse(d.readFileSync(p.join(t,e),"utf8"))}catch{return{}}},"readTicket"),M=s((t,{first:e})=>`${e?`0${String(1e15-Date.parse(e)).padStart(16,"0")}`:"1"}-${t}`,"rank");function R(t){const e=I(t);let n=[];try{n=d.readdirSync(e).filter(r=>!r.endsWith(".tmp"))}catch{}return n.filter(r=>E(K(r))).map(r=>({ticket:r,...gt(e,r)})).sort((r,o)=>M(r.ticket,r)<M(o.ticket,o)?-1:1)}s(R,"queuedAsks");const B=s((t,e,n)=>{const r=p.join(t,`${e}.tmp`);d.writeFileSync(r,JSON.stringify(n)),d.renameSync(r,p.join(t,e))},"writeTicket");let kt=0;async function yt(t,e,n,{onWait:r,asked:o=Date.now(),site:i=null,key:a=null,first:u=null}={}){const $=p.join(N(),`${t}.lock`),h=I(t);d.mkdirSync(h,{recursive:!0});const k=`${String(o).padStart(15,"0")}-${String(kt++).padStart(6,"0")}-${process.pid}`;B(h,k,{card:e,site:i,key:a,first:u});let g=null;try{for(;;){const w=R(t),S=w.findIndex(v=>v.ticket===k),y=w.slice(0,S);let l=null;if(!y.length)try{d.writeFileSync($,JSON.stringify({card:e,pid:process.pid,at:new Date().toISOString()}),{flag:"wx"});break}catch(v){if(v.code!=="EEXIST")throw v;try{l=JSON.parse(d.readFileSync($,"utf8"))}catch{}if(!(Number.isInteger(l?.pid)&&E(l.pid))){d.rmSync($,{force:!0});continue}}const f=l?.card??(y.length?`the ${t} ahead`:null);f&&f!==g&&(g=f,await r?.(f)),await J([y.length?p.join(h,y.at(-1).ticket):$,p.join(h,k)])}}finally{d.rmSync(p.join(h,k),{force:!0})}try{return await n()}finally{try{JSON.parse(d.readFileSync($,"utf8")).pid===process.pid&&d.rmSync($,{force:!0})}catch{}}}s(yt,"holdingQueuedLock");const wt=s((t,e,n)=>yt("land",t,e,n),"holdingLandLock");function Mt(t,e,n){const r=I("land"),o=R("land").filter(i=>i.key===e&&i.site&&st(i.site,t));for(const{ticket:i,...a}of o)B(r,i,{...a,first:n});return o.length}s(Mt,"moveQueuedLand");function Bt(){const t=St();return[...t?[{card:t.card,running:!0,first:null}]:[],...R("land").map(({card:e,first:n})=>({card:e,running:!1,first:n??null}))]}s(Bt,"landQueue");function St(){try{const t=JSON.parse(d.readFileSync(p.join(N(),"land.lock"),"utf8"));return Number.isInteger(t?.pid)&&E(t.pid)?t:null}catch{return null}}s(St,"landLock");async function _t(t,e,{merge:n=!1}={}){const r=D();d.mkdirSync(r,{recursive:!0});const o=p.join(r,encodeURIComponent(t));let i=!1;for(;;){if(i&&!d.existsSync(o))return{joined:!0};try{d.writeFileSync(o,JSON.stringify({pid:process.pid,at:new Date().toISOString(),...n?{merge:n}:{}}),{flag:"wx"});break}catch(a){if(a.code!=="EEXIST")throw a;let u=null;try{u=JSON.parse(d.readFileSync(o,"utf8"))}catch{}if(!(Number.isInteger(u?.pid)&&E(u.pid))){d.rmSync(o,{force:!0});continue}i=!0,await J([o])}}try{return await e()}finally{d.rmSync(o,{force:!0})}}s(_t,"joiningLand");function Ht(t){try{const e=JSON.parse(d.readFileSync(p.join(D(),encodeURIComponent(t)),"utf8"));return Number.isInteger(e?.pid)&&E(e.pid)?e:null}catch{return null}}s(Ht,"runningLand");function Ut(t,e){const n=p.join(D(),encodeURIComponent(t));try{e.includes(JSON.parse(d.readFileSync(n,"utf8")).pid)&&d.rmSync(n,{force:!0})}catch{}let r=[];try{r=d.readdirSync(I("land"))}catch{}for(const o of r.filter(i=>e.includes(K(i))))d.rmSync(p.join(I("land"),o),{force:!0})}s(Ut,"clearStoppedLand");function vt(t,e,n){const o=c(t,"diff","--name-only","--no-renames","--diff-filter=A",e,n,"--","docs/adr").out.split(`
|
|
20
|
+
`).find(i=>/^docs\/adr\/\d{4}-(?!\d{2}-\d{2}-\d{4}-)/.test(i));o&&m(`land: ${o} is named with a number; name it docs/adr/YYYY-MM-DD-HHMM-${p.basename(o).replace(/^\d{4}-/,"")}, the time you wrote it, and land again`)}s(vt,"refuseNumberedAdr");function xt(t,e,n,r){const o=c(t,"log","--format=%h",`${e}..${n}`).out.split(`
|
|
21
|
+
`).filter(Boolean);for(const i of o){const a=c(t,"log","-1","--format=%s",i).out;r&&!a.startsWith(`${r}: `)&&m(`land: commit ${i} "${a}" doesn't start with "${r}: "; reword it and land again`),/^# Conflicts:/m.test(c(t,"log","-1","--format=%B",i).out)&&m(`land: commit ${i} "${a}" still holds a "# Conflicts:" note in its message; amend it out and land again`)}}s(xt,"refuseBadMessages");function Yt(t,e){const n=c(t,"fetch","--quiet","--no-tags","origin",`+refs/heads/${e}:refs/remotes/origin/${e}`);if(n.ok||m(`could not fetch ${e} from origin into ${t}: ${n.err}`),c(t,"symbolic-ref","--quiet","--short","HEAD").out!==e)return;const r=c(t,"merge","--quiet","--ff-only",`refs/remotes/origin/${e}`);r.ok||m(`${e} in ${t} doesn't fast-forward to origin's (${r.err}); bring it level with origin/${e} and land again`)}s(Yt,"syncBase");async function jt(t,{say:e=console.log,runTests:n=A,outcome:r}){const o=await n(t),i=o.log?[`Full test output: ${o.log}`]:o.logError?[o.logError]:[];o.hung&&m([`tests hung past ${o.hung.after} in ${o.hung.files.join(", ")||"the test runner"}; ${r}`,...i].join(`
|
|
22
|
+
`)),o.ok||m([`tests failed in ${t} (${o.fail} failing); ${r}`,...o.failures?.length?o.failures:[o.error||"test runner failed without test diagnostics"],...i].join(`
|
|
23
|
+
`)),e(`tests passed: ${o.pass}`)}s(jt,"passTests");async function _({path:t,branch:e,repo:n,base:r,ref:o},{say:i=console.log,runTests:a=A}={}){if(d.existsSync(t)||m(`no worktree at ${t}`),H(n,r),c(t,"status","--porcelain").out&&m(`${t} has uncommitted changes; commit them there first`),Number(c(n,"rev-list","--count",`${r}..${e}`).out)===0&&m(`${e} has no commits beyond ${r}; nothing to land`),!c(n,"merge-base","--is-ancestor",r,e).ok){const h=c(t,"rebase","--quiet",r);if(!h.ok){const k=c(t,"diff","--name-only","--diff-filter=U").out.split(`
|
|
24
|
+
`).filter(Boolean);c(t,"rebase","--abort"),k.length||m(`rebasing ${e} onto ${r} failed: ${h.err}`),m(`rebasing ${e} onto ${r} conflicts in ${k.join(", ")}; resolve it in ${t} and land again`)}i(`rebased ${e} onto ${r} (${L(n,r)})`)}const u=c(n,"log","--format=%h %s",`${r}..${e}`).out.split(`
|
|
25
|
+
`).filter(h=>h&&!h.includes(" "));u.length&&m(`${e} has a commit with an empty subject (${u[0]}); reword it in ${t} and land again`),vt(n,r,e),xt(n,r,e,o);const $=c(n,"rev-parse",e).out;return c(n,"rev-parse","--verify","--quiet",F(e)).out===$?(i(`${e} was tested on ${r} as it is now; not testing it again`),$):(await jt(t,{say:i,runTests:a,outcome:"nothing landed"}),c(n,"update-ref",F(e),$),$)}s(_,"prepareLand");const F=s(t=>`refs/kickflip/tested/${t}`,"testedRef");function H(t,e){const n=c(t,"symbolic-ref","--quiet","--short","HEAD").out;n!==e&&m(`${t} is on ${n||"a detached HEAD"}, not ${e}; switch it to ${e} (git -C ${t} switch ${e}) and land again`)}s(H,"onBase");async function Gt(t,{hold:e=!1,onWait:n,sync:r=s(()=>{},"sync"),queue:o={},...i}={}){const{say:a=console.log}=i,u=Date.now();for(;;){const $=await _(t,i);if(e||await wt(t.ref??t.branch,()=>(r(),U(t,$,i)),{onWait:n,asked:u,...o}))return;a(`${t.base} moved while ${t.branch} was tested; testing it on ${t.base} again`)}}s(Gt,"landInTurn");async function Qt(t,{hold:e=!1,...n}={}){const r=await _(t,n);e||U(t,r,n)||m(`${t.base} moved while ${t.branch} was tested; land again`)}s(Qt,"land");function U({path:t,branch:e,repo:n,base:r},o,{say:i=console.log,restart:a=pt,boards:u=s(()=>rt(),"boards"),clone:$=C()}={}){if(H(n,r),c(n,"rev-parse",e).out!==o||!c(n,"merge-base","--is-ancestor",r,o).ok)return!1;const h=c(n,"diff","--name-only",r,e).out.split(`
|
|
26
|
+
`).filter(Boolean),k=c(n,"merge","--quiet","--ff-only",o);k.ok||m(`could not fast-forward ${r} in ${n}: ${k.err}`),i(`${r} fast-forwarded to ${L(n,r)} in ${n}`);const g=[],w=d.realpathSync(n)===d.realpathSync($)?h.filter(f=>!/\.test\.mjs$/.test(f)):[],S=new Set(w.length?nt(p.join(n,"hub","hub.mjs")).files:[]);if(w.some(f=>f.startsWith("hub/")||S.has(f))){const f=a("hub");f?g.push(`the hub didn't restart (${f})`):i("hub restarted, with every site")}else if(w.some(f=>f.startsWith("board/")))for(const f of u()){const v=a("board",f.route);v?g.push(`/${f.route} didn't restart (${v})`):i(`board restarted: ${f.route}`)}const y=c(n,"worktree","remove",t);y.ok?i(`worktree removed: ${t}`):g.push(`could not remove ${t} (${y.err})`);const l=c(n,"branch","--quiet","-d",e);return l.ok?i(`branch deleted: ${e}`):g.push(`could not delete branch ${e} (${l.err})`),c(n,"update-ref","-d",F(e)),g.length&&m(`landed, but ${g.join("; ")}`),!0}s(U,"landPrepared");export{Pt as boardWorktree,P as builderRepo,Ut as clearStoppedLand,c as git,At as helperWorktree,wt as holdingLandLock,_t as joiningLand,Qt as land,Gt as landInTurn,St as landLock,U as landPrepared,Bt as landQueue,W as makeWorktree,Jt as mergeHelper,Mt as moveQueuedLand,A as nodeTests,jt as passTests,Kt as prWorktree,_ as prepareLand,pt as restartRunner,Ht as runningLand,C as skillRepo,Yt as syncBase,O as worktreesDir};
|
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// The pause line's hook (docs/adr/2026-11-01-1710-work-pauses-at-the-pause-line.md): SessionStart and UserPromptSubmit
|
|
3
|
+
// say paused or Keep going, and PreToolUse denies new agents while paused. A board's session also reads its board's own
|
|
4
|
+
// pause, the board being the manifest site its session owns
|
|
5
|
+
// (docs/adr/2026-11-05-1849-pause-board-and-pause-hub-pause-new-work-by-hand.md).
|
|
2
6
|
import fs from 'node:fs';
|
|
3
|
-
import
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { readUsageGate, pauseReason, pauseUntilWords } from '../../shared/usage-gate.mjs';
|
|
10
|
+
import { FOREVER } from '../../hub/duration.mjs';
|
|
4
11
|
|
|
12
|
+
function boardDirOf(session) {
|
|
13
|
+
try {
|
|
14
|
+
const sites = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude', 'kickflip', 'manifest.json'), 'utf8')).sites || {};
|
|
15
|
+
return Object.values(sites).find((s) => s?.owner?.session === session)?.dir ?? null;
|
|
16
|
+
} catch { return null; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MANUAL = { hub: 'The owner paused the hub', board: 'The owner paused this board' };
|
|
5
20
|
process.stdout.on('error', () => {});
|
|
6
21
|
try {
|
|
7
22
|
const input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
8
|
-
const gate = readUsageGate();
|
|
23
|
+
const gate = readUsageGate({ dir: input.session_id ? boardDirOf(input.session_id) : null });
|
|
9
24
|
if (input.hook_event_name === 'PreToolUse') {
|
|
10
25
|
if (gate.state === 'paused' && ['Agent', 'Task'].includes(input.tool_name)) console.log(JSON.stringify({ hookSpecificOutput: {
|
|
11
26
|
hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: pauseReason(gate),
|
|
12
27
|
} }));
|
|
13
28
|
} else if (['SessionStart', 'UserPromptSubmit'].includes(input.hook_event_name)) {
|
|
14
|
-
|
|
29
|
+
const why = gate.manual ? MANUAL[gate.manual] : `Claude's usage is past the pause line (${gate.pause_at}%)`;
|
|
30
|
+
const answer = gate.resets_at === FOREVER ? '"paused until you resume"' : `"paused until <that time, in the owner's local time>"`;
|
|
31
|
+
if (gate.state === 'paused') console.log(`${why}. Running steps finish; start no new work, builders or agents ${pauseUntilWords(gate.resets_at)}; answer the owner's messages briefly with ${answer}.`);
|
|
15
32
|
else if (gate.state === 'going') console.log(`Keep going is on: work runs past the pause line until ${gate.resets_at}, spending usage.`);
|
|
16
33
|
}
|
|
17
34
|
} catch {}
|
package/hub/board-repo.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var S=Object.defineProperty;var d=(e,r)=>S(e,"name",{value:r,configurable:!0});import{readManifest as h,withManifestLock as f,writeManifest as g,isBoardSite as l,listBoards as v,appendEvent as C,buildsLocally as E}from"./boards.mjs";import{routeRecord as m}from"./routes.mjs";import{repoService as H,validateBranch as I,runRepo as O,githubFromOrigin as j,defaultBranch as L}from"./repos.mjs";import{repoInput as M}from"./new-board.mjs";import{boardConfig as D}from"../board/store.mjs";import{storedPath as p}from"../bin/install.mjs";import{cloneRoot as F}from"../shared/repos.mjs";import{githubFromOrigin as Y}from"./repos.mjs";const w=d((e,r=400)=>{throw Object.assign(new Error(e),{status:r})},"refuse");function B(e,r=h()){const o=m(r.sites,e);l(o)||w("Board not found.",404);const a=E(o);return{github:o.github||"",...o.remote?{remote:o.remote}:{},branch:o.branch||"",repo:p(o.owner?.repo||""),...a?{local:!0}:{},...o.hub===!0?{hub:!0}:{}}}d(B,"boardRepo");const N=d(e=>e.local?`refs/heads/${e.branch||"main"}`:`refs/remotes/origin/${e.branch||"main"}`,"mainRefOf");async function Q(e,r,{repos:o=H(),event:a=C}={}){B(e),(!r||typeof r!="object"||Array.isArray(r)||Object.keys(r).some(i=>!["github","remote","branch"].includes(i)))&&w("Send only github or remote, and branch.");const{github:n="",remote:t}=M(r),s=I(r.branch),u=m(h().sites,e).dir;t&&(await D(u)).tickets?.source==="github"&&w("This board files its cards as GitHub issues. Switch Tickets to Internal or Linear first.",409);const c=p(await o.ensure(n||t,s));return f(async()=>{const i=h(),A=B(e,i),b=m(i.sites,e);Object.assign(b,{github:n,remote:t,branch:s,updated:new Date().toISOString()}),delete b.local,n||delete b.github,t||delete b.remote,b.owner={...b.owner,repo:c},await g(i);const R=v(i).find(x=>x.route===e),y={github:n,...t?{remote:t}:{},branch:s,repo:c},k=await a(R,"repo-changed",{...y,previous:A});return{...y,id:k}})}d(Q,"changeBoardRepo");async function U({run:e=O}={}){return f(async()=>{const r=h(),o=Array.isArray(r.migrated)?r.migrated:[];if(o.includes("board-github")||!Object.keys(r.sites||{}).length)return[];const a=[];for(const[n,t]of Object.entries(r.sites||{})){if(!l(t)||t.github)continue;const s=t.owner?.repo||t.dir,u=await e("git",["-C",s,"remote","get-url","origin"]),c=u.status===0?j(u.stdout):null;if(!c)continue;t.github=c;const i=t.branch||await L(s,e);i&&(t.branch=i),a.push(n)}return r.migrated=[...o,"board-github"],await g(r),a})}d(U,"migrateBoardRepos");async function V({run:e=O,clone:r=F()}={}){return f(async()=>{const o=h(),a=Array.isArray(o.migrated)?o.migrated:[];if(a.includes("hub-local")||!Object.keys(o.sites||{}).length)return null;const n=Object.entries(o.sites||{}).find(([,t])=>t?.hub===!0&&l(t)&&t.local!==!0);if(n){const[,t]=n,s=t.owner?.repo||p(r),u=await e("git",["-C",s,"remote","get-url","origin"]),c=t.github||(u.status===0?j(u.stdout):null),i=await e("git",["-C",s,"symbolic-ref","--quiet","--short","HEAD"]);Object.assign(t,{local:!0,...c?{github:c}:{},...!t.branch&&i.status===0&&i.stdout.trim()?{branch:i.stdout.trim()}:{},owner:{...t.owner,repo:s}})}return o.migrated=[...a,"hub-local"],await g(o),n?n[0]:null})}d(V,"migrateHubBoard");export{B as boardRepo,Q as changeBoardRepo,Y as githubFromOrigin,N as mainRefOf,U as migrateBoardRepos,V as migrateHubBoard};
|
package/hub/boards.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var q=Object.defineProperty;var
|
|
2
|
-
`),
|
|
3
|
-
`),s.id}
|
|
4
|
-
`).filter(Boolean)}catch{}return[n,t?r.filter(s=>t.test(s)):r]}))}
|
|
1
|
+
var q=Object.defineProperty;var i=(e,t)=>q(e,"name",{value:t,configurable:!0});import f from"node:fs";import c from"node:path";import z from"node:os";import Z from"node:crypto";import{fileURLToPath as Q}from"node:url";import{routeRecord as d}from"./routes.mjs";import{validBuildTarget as X}from"../board/build-script.mjs";import{deliver as Y,readSettings as ee}from"./push.mjs";import{SLOWEST_CHECK_MINUTES as te}from"./schedule.mjs";import{liveSessions as v,ps as ne}from"./live.mjs";import{withFileLock as re}from"./locks.mjs";import{dropSessionPrompts as se,readPrompts as oe,savePrompts as ie,withPromptsLock as ae}from"./prompts.mjs";import{storedPath as S}from"../bin/install.mjs";import{hubService as ce}from"../shared/service.mjs";import"../shared/repos.mjs";import"./repos.mjs";import{prLinks as ue,reviewLinks as le,topicItems as pe}from"../board/review.mjs";import{cardPRs as fe}from"../shared/card-prs.mjs";const de=c.resolve(c.dirname(Q(import.meta.url)),".."),C=c.join(z.homedir(),".claude"),m=c.join(C,"kickflip"),F=c.join(C,"waves"),$=c.join(m,"manifest.json"),he=c.join(m,"github-state.json"),me=c.join(m,"wake-state.json"),ut=c.join(m,"incoming-files.json"),P=c.join(m,"deaf-state.json"),E=c.join(m,"resume-state.json"),x=c.join(m,"link-faults.json"),we=c.join(m,"repo-fetch.json"),M=c.join(m,"hub-code.json"),ge="1970-01-01T00:00:00Z",ke=2*6e4,je=15*6e4,p=i((e,t)=>{try{return JSON.parse(f.readFileSync(e,"utf8"))}catch{return t}},"readJSON");function k(e,t){const n=`${e}.${process.pid}.tmp`;f.mkdirSync(c.dirname(e),{recursive:!0}),f.writeFileSync(n,JSON.stringify(t,null,2)+`
|
|
2
|
+
`),f.renameSync(n,e)}i(k,"saveJSON");const j=i(()=>p($,{sites:{}}),"readManifest"),lt=i(e=>re($,e),"withManifestLock");function pt(e){return ae(()=>{const t=[...Object.values(e.sites||{}),...Object.values(e.closed||{}).map(o=>o.site)];for(const o of t.filter(Boolean))o.dir&&(o.dir=S(o.dir)),Array.isArray(o.cmd)&&(o.cmd=o.cmd.map(l=>S(l))),o.owner?.repo&&(o.owner.repo=S(o.owner.repo));for(const o of Object.values(e.starting||{}))o.repo&&(o.repo=S(o.repo));const n=j(),r=i((o,l)=>d(o.sites,l)?.owner||d(o.starting,l),"owner"),s=new Set([...Object.keys(n.sites||{}),...Object.keys(e.sites||{}),...Object.keys(n.starting||{}),...Object.keys(e.starting||{})]),a=[];for(const o of s){const l=r(n,o),u=r(e,o);(l?.session!==u?.session||l?.since!==u?.since)&&a.push(l?.session,u?.session)}if(a.some(Boolean)){const o=oe();se(o,a),ie(o)}k($,e)})}i(pt,"writeManifest");const _=i(e=>`${e.repo}#${e.pr}`,"prKey"),B=i((e,t)=>`${e} ${t}${e===1?"":"s"}`,"plural"),U=i(e=>(Array.isArray(e)?e:[]).map(t=>typeof t=="string"?t:t?.name).filter(t=>typeof t=="string"&&t),"names"),be=i(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=i(e=>Array.isArray(e?.items),"isWave"),J=/board[/\\]server\.mjs$/,A=i(e=>Array.isArray(e?.cmd)&&e.cmd.some(t=>J.test(t))&&typeof e.dir=="string","isBoardSite"),ye=i(e=>e.local===!0||!(e.github||e.remote),"buildsLocally");function ve(e,t,n){if(n==="file"){const s=p(c.resolve(e,t.items),null);return H(s)?[{slug:t.slug||c.basename(t.items,".json"),wave:s}]:[]}let r=[];try{r=f.readdirSync(F,{withFileTypes:!0}).filter(s=>s.isDirectory()).map(s=>s.name).sort()}catch{}return r.map(s=>({slug:s,wave:p(c.join(F,s,"wave.json"),null)})).filter(s=>H(s.wave))}i(ve,"wavesOf");function Se(e,t){return e.flatMap(({slug:n,wave:r})=>r.items.filter(s=>s&&s.lane!=="done").flatMap(s=>fe(s,t||r.repo).map(a=>({wave:n,id:s.id,pr:a.number,repo:a.repo,since:s.gh_since||ge}))))}i(Se,"prsOf");function T(e,t){const n=p(c.join(t.dir,"board.json"),{}),r=typeof n.items=="string"&&n.items!=="waves"?"file":"waves",s=ve(t.dir,n,r),a=t.github;return{route:e,dir:t.dir,port:t.port,title:n.title||t.title||e,kind:r,waves:s,prs:Se(s,a),github:a,...t.remote&&!a?{remote:t.remote}:{},branch:t.branch,...ye(t)?{local:!0}:{},...t.hub===!0?{hub:!0}:{},auto_build:n.auto_build===!0,...X(n.build_target)?{build_target:n.build_target}:{},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:U(n.reviewers),reviewerLogins:be(n.reviewers),owner:t.owner||null,watch:Array.isArray(t.watch)&&t.watch.length?t.watch:["data/events.jsonl"],filter:t.filter||null}}i(T,"boardOf");function N(e=j()){return Object.entries(e?.sites||{}).filter(([,t])=>A(t)).map(([t,n])=>T(t,n))}i(N,"listBoards");function ft(e=j()){const t=Object.entries(e?.sites||{}).find(([,n])=>n?.hub===!0&&A(n));return t?T(...t):void 0}i(ft,"hubBoard");function Oe(e,t){const n=i(r=>{try{return f.realpathSync(r)}catch{return c.resolve(r)}},"real");return c.resolve(e)===c.resolve(t)||n(e)===n(t)}i(Oe,"samePath");function dt(e,t=j()){const n=Object.entries(t?.sites||{}).find(([,r])=>A(r)&&Oe(r.dir,e));return n?T(...n):null}i(dt,"boardAt");async function W(e,t,n,{timeout:r=3e4}={}){if(e.port){let a;try{a=await fetch(`http://127.0.0.1:${e.port}/_claude/events?board=0`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({kind:t,...n}),signal:AbortSignal.timeout(r)})}catch(o){if(o.name==="TimeoutError")throw new Error(`/${e.route} didn't answer within ${r/1e3} s, so it may already have the ${t} event`);if(o.cause?.code!=="ECONNREFUSED"&&o.cause?.message!=="bad port")throw new Error(`/${e.route} dropped the ${t} event's request (${o.cause?.code||o.cause?.message||o.message}), so it may already have it`)}if(a){if(a.ok)return(await a.json()).id;const o=await a.json().then(l=>l.error,()=>null);throw new Error(`/${e.route} answered ${a.status}${o?` (${o})`:""}, so it may already have the ${t} event`)}}const s={id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),ts:new Date().toISOString(),kind:t,data:n};return f.mkdirSync(c.join(e.dir,"data"),{recursive:!0}),f.appendFileSync(c.join(e.dir,"data","events.jsonl"),JSON.stringify(s)+`
|
|
3
|
+
`),s.id}i(W,"appendEvent");async function ht(e,t,n){const r=[],s=[];for(const a of e)try{r.push({route:a.route,id:await W(a,t,n)})}catch(o){s.push(`${a.route}: could not add the ${t} event (${o.message})`)}return{events:r,failed:s}}i(ht,"sendToBoards");function $e(e,t=Date.now(),{grace:n=ke}={}){const r=p(c.join(e,"data","watcher.json"),null);if(!Number.isInteger(r?.pid))return!1;if(typeof r.exited_at=="string")return t-Date.parse(r.exited_at)<=n;try{process.kill(r.pid,0)}catch(s){if(s.code!=="EPERM")return!1}return G(r.pid)?.includes("wait-events.mjs")??!0}i($e,"watcherRunning");const G=i(e=>ne(["-p",String(e),"-o","command="]),"commandOf"),K=i(e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"escaped");function mt(e,t,n,{command:r=G,kill:s=process.kill}={}){const a=p(c.join(e,"data","watcher.json"),null);if(!Number.isInteger(a?.pid)||a.pid===process.pid||typeof a.exited_at=="string"||!new RegExp(`wait-events\\.mjs['"]?\\s+['"]?${K(t)}['"]?\\s+['"]?${K(n)}['"]?(\\s|$)`).test(r(a.pid)??""))return null;try{s(a.pid,"SIGTERM")}catch{return null}return a.pid}i(mt,"stopPreviousWatcher");function Ee(e,{now:t=Date.now(),running:n=v()}={}){const r=e.owner?.session;return!!(r&&(n?.has(r)??!0))&&$e(e.dir,t)}i(Ee,"isListening");function L(e,{now:t=Date.now(),running:n=v(),wakes:r=p(E,{})}={}){if(Ee(e,{now:t,running:n}))return"listening";const s=e.owner?.session,a=d(r,e.route);return!s||!e.owner.repo||a?.error||a?.pushed_at?"deaf":a?.resumed_at?"waking":n?.has(s)?"deaf":"asleep"}i(L,"listenState");const R=i((e,t)=>c.join(e.dir,"data",`.watch-cursor-${t}.json`),"cursorPath"),O=i(e=>R(e,e.owner.session),"cursorFile");function wt(e){const t=e.owner?.session?p(O(e),{}):{};return Object.fromEntries(e.watch.map(n=>{const r=d(t,n);return[n,Number.isSafeInteger(r)&&r>=0?r:0]}))}i(wt,"watcherCursor");const gt=i(e=>Object.fromEntries(Object.entries(D(e)).map(([t,n])=>[t,n.length])),"watchedCounts");function kt(e,{from:t,to:n=t,mark:r=null}){const s=p(R(e,t),null);if(!s&&!r)return;const a=Object.assign(Object.create(null),s);for(const[o,l]of Object.entries(r||{}))a[o]=Math.min(d(a,o)??l,l);k(R(e,n),a)}i(kt,"carryCursor");function D(e){const t=e.filter?new RegExp(e.filter):null;return Object.fromEntries(e.watch.map(n=>{let r=[];try{r=f.readFileSync(c.resolve(e.dir,n),"utf8").split(`
|
|
4
|
+
`).filter(Boolean)}catch{}return[n,t?r.filter(s=>t.test(s)):r]}))}i(D,"watchedLines");function xe(e){if(!e.owner?.session)return null;const t=p(O(e),null),n=t?0:Date.parse(e.owner.since||"")||0;let r=null;for(const[s,a]of Object.entries(D(e)))for(const o of t?a.slice(d(t,s)||0):a){let l=null;try{l=JSON.parse(o).ts}catch{}const u=Date.parse(l);Number.isNaN(u)||u<n||(!r||u<Date.parse(r))&&(r=l)}return r}i(xe,"oldestUnheard");async function jt({manifest:e,now:t=new Date,running:n=v(),wakes:r=p(E,{})}={}){if(!n)return{pushed:[],failed:[]};const s=Object.assign(Object.create(null),p(P,{})),a=[],o=[];let l=!1;for(const u of N(e)){const h=L(u,{now:t.getTime(),running:n,wakes:r});if(h==="listening"){d(s,u.route)&&(delete s[u.route],l=!0);continue}if(h==="asleep"||h==="waking"||d(s,u.route))continue;const w=xe(u);if(!w||t-Date.parse(w)<je)continue;const y=Math.round((t-Date.parse(w))/6e4),g=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 Y({title:`${u.title} isn't listening`,body:`An event has waited ${y} min and ${g}`,url:`/${u.route}/`,tag:`deaf-${u.route}`},{kind:"fault",immediate:!0,at:t}),s[u.route]={at:t.toISOString(),since:w},l=!0,a.push(u.route)}catch(V){o.push(`${u.route}: could not push that it isn't listening (${V.message})`)}}return l&&k(P,s),{pushed:a,failed:o}}i(jt,"checkListening");const _e=i(e=>c.join(e,"data","handshake.json"),"handshakeFile");async function bt(e,t=j()){const n=N(t).find(s=>s.route===e);if(!n)throw new Error(`/${e} is not a board site in the manifest`);n.owner?.session&&!f.existsSync(O(n))&&k(O(n),Object.fromEntries(Object.entries(D(n)).map(([s,a])=>[s,a.length])));const r={id:await W(n,"ping",{title:n.title}),at:new Date().toISOString(),session:n.owner?.session||null};return k(_e(n.dir),{ping:r,ack:null}),{route:e,ping:r.id}}i(bt,"handshake");const yt=["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 r=e.prs.filter(a=>t.prs?.[_(a)]?.at!==n.at).map(a=>`#${a.pr}`);if(r.length)return{link:"github",ok:!1,detail:`${[...new Set(r)].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 s=new Set(e.prs.map(_)).size;return{link:"github",ok:!0,detail:s?`${B(s,"open PR")}, all in the last check.`:"No open PRs; the last check reached this board."}}i(Ae,"githubLink");function Te(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}i(Te,"repoFetchLink");const Ne={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."}},Le=i(e=>({link:"events",listen:e,...Ne[e]}),"eventsLink");function Re(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.`}}i(Re,"wakesLink");function De(e,t,n){const r=p(c.join(e.dir,"data","pr-slack.json"),{}),s=[];let a=0;for(const o of e.prs){const l=t?.prs?.[_(o)];if(l?.state!=="OPEN"||l.isDraft)continue;a++;const{wave:u}=e.waves.find(g=>g.slug===o.wave),h=u.items.find(g=>g.id===o.id),w=le(u,h,`${o.wave}/${o.id}`,n,ue(r,[{repo:o.repo,number:o.pr}])),y=[...new Set([...e.reviewers,...U(u.reviewers)])].filter(g=>!Object.hasOwn(w,g));y.length&&s.push(`#${o.pr} has no link for ${y.join(", ")}`)}return s.length?{link:"slack",ok:!1,detail:`${s.join("; ")}.`}:{link:"slack",ok:!0,detail:a?`Every reviewer has the link to ${B(a,"PR")} in review.`:"No PR is in review."}}i(De,"slackLink");const Ie=/\b(?:from|import)\s*['"](\.\.?\/[^'"]+)['"]/g;function I(e=c.join(de,"hub","hub.mjs")){const t=c.dirname(c.dirname(e)),n=new Map,r=i(o=>{if(n.has(o))return;let l;try{l=f.readFileSync(o)}catch{return}n.set(o,l);for(const u of l.toString("utf8").matchAll(Ie))r(c.resolve(c.dirname(o),u[1]))},"visit");r(e);const s=[...n.keys()].map(o=>c.relative(t,o)).sort(),a=Z.createHash("sha256");for(const o of s)a.update(o).update("\0").update(n.get(c.join(t,o))).update("\0");return{hash:a.digest("hex").slice(0,12),files:s}}i(I,"hubCode");const vt=i(e=>I(c.resolve(e.cwd||e.dir,e.cmd.find(t=>J.test(t)))).hash,"boardCode");function St(){const e={...I(),pid:process.pid,at:new Date().toISOString()};return k(M,e),e}i(St,"recordHubCode");const Ce=ce().restartCommand,Fe=i(()=>p(M,null)?.hash??null,"runningHubCode"),Pe=i(()=>({running:Fe(),onDisk:I().hash}),"hubCodeState"),Ot=i(({running:e,onDisk:t}=Pe())=>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 ${Ce}`},"hubLink"),b=6e4,Me={github:i(e=>(e+10)*b,"github"),"repo-fetch":i(e=>(e+10)*b,"repo-fetch"),events:i(()=>2*b,"events"),wakes:i(e=>(e+10)*b,"wakes"),slack:i(e=>(e+10)*b,"slack")};function Be(e,t,{now:n,faults:r}){const s=r===void 0,a=s?d(p(x,{}),e)||{}:r,o={},l=t.map(u=>{if(u.ok)return{...u,state:"ok"};const h=d(a,u.link)||new Date(n).toISOString();o[u.link]=h;const w=Me[u.link](te);return{...u,state:n-Date.parse(h)>=w?"broken":"settling",since:h}});if(s&&JSON.stringify(o)!==JSON.stringify(a)){const u=Object.assign(Object.create(null),p(x,{}));Object.keys(o).length?u[e]=o:delete u[e];try{k(x,u)}catch{}}return l}i(Be,"settle");function Ue(e,t={}){const n=t.github??p(he,{}),r=t.settings??ee(),s=[Ae(e,n),Te(d(t.fetches??p(we,{}),e.route)),Le(t.listen??L(e)),Re(e,r,t.wakes??p(me,{})),De(e,n,t.topics??pe())].filter(Boolean);return Be(e.route,s,{now:t.now??Date.now(),faults:t.faults})}i(Ue,"linksOf");function $t(e){const t={running:v(),wakes:p(E,{})};if(t.running)for(const n of N(e))Ue(n,{listen:L(n,t)})}i($t,"recordLinkFaults");export{je as DEAF_AFTER,he as GITHUB_STATE,Me as GRACE,M as HUB_CODE,ut as INCOMING_FILES,yt as LINKS,we as REPO_FETCH,E as RESUME_STATE,me as WAKE_STATE,F as WAVES,W as appendEvent,dt as boardAt,vt as boardCode,ye as buildsLocally,kt as carryCursor,jt as checkListening,bt as handshake,_e as handshakeFile,ft as hubBoard,I as hubCode,Pe as hubCodeState,Ot as hubLink,A as isBoardSite,Ee as isListening,Ue as linksOf,N as listBoards,L as listenState,xe as oldestUnheard,_ as prKey,Se as prsOf,p as readJSON,j as readManifest,St as recordHubCode,$t as recordLinkFaults,be as reviewerLogins,Fe as runningHubCode,Oe as samePath,k as saveJSON,ht as sendToBoards,mt as stopPreviousWatcher,gt as watchedCounts,D as watchedLines,wt as watcherCursor,$e as watcherRunning,lt as withManifestLock,pt as writeManifest};
|
package/hub/front-door.mjs
CHANGED
|
@@ -9,7 +9,7 @@ ${b}</style></head><body>
|
|
|
9
9
|
<img class="rf-mark" src="data:image/png;base64,${i}" width="70" height="60" alt="">
|
|
10
10
|
<p class="rf-line">${e}</p>
|
|
11
11
|
<p class="rf-sub">${n}</p>
|
|
12
|
-
</body></html>`}a(w,"refusalPage");function S(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(S,"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
|
|
12
|
+
</body></html>`}a(w,"refusalPage");function S(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",...n==="other"?{"x-kickflip-down":"access"}:{}}),e.end(l?w(n):JSON.stringify(i)),t.resume()}a(S,"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
13
|
Content-Type: application/json\r
|
|
14
14
|
Content-Length: ${Buffer.byteLength(i)}\r
|
|
15
15
|
Connection: close\r
|
package/hub/github.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var le=Object.defineProperty;var S=(e,s)=>le(e,"name",{value:s,configurable:!0});import x from"node:fs";import U from"node:path";import"node:os";import{savedClient as fe}from"./linear.mjs";import{checksPassed as V,fetchPRs as ue,markReady as me,reportOf as ge}from"./pr-report.mjs";import{wakesDue as pe,localDay as he}from"./schedule.mjs";import{GITHUB_STATE as F,WAKE_STATE as Q,WAVES as X,listBoards as Y,appendEvent as Z,prKey as M,readJSON as ee,saveJSON as te,reviewerLogins as we}from"./boards.mjs";import{appendThread as H,setItem as se,mergedLine as ye,mergeCard as ve}from"../board/store.mjs";import{allLanded as re,cardPRs as ke,leastReady as $e,prNames as Se}from"../shared/card-prs.mjs";const ne=["MERGED","CLOSED"];import{runOwnerChecks as De}from"./owner-checks.mjs";import{audit as b,boardAudit as T,hubAudit as ae}from"../shared/audit.mjs";const _=["ci","review","comments","merged","mergeable","draft","linked"],G=S(e=>e&&e!=="UNKNOWN"?e:null,"known");function Ee(e){const n=(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:n?String(n.id):null,eventAt:n?.at||null,title:e.title??null,additions:e.additions??null,deletions:e.deletions??null,files:e.files??null,approved:!!e.approved,commits:Array.isArray(e.commits)?e.commits:[]}}S(Ee,"summarize");function Re(e,s){const n=new Set;if(!e)return s.state==="MERGED"&&n.add("merged"),s.eventId&&n.add("comments"),_.filter(a=>n.has(a));(s.pending===0&&(e.pending>0||e.head!==s.head)||e.failing.join(`
|
|
2
2
|
`)!==s.failing.join(`
|
|
3
|
-
`))&&n.add("ci"),e.reviewDecision!==s.reviewDecision&&n.add("review"),s.eventId&&s.eventId!==e.eventId&&n.add("comments"),s.state==="MERGED"&&e.state!=="MERGED"&&n.add("merged");for(const a of["mergeable","mergeStateStatus"])G(e[a])&&G(s[a])&&e[a]!==s[a]&&n.add("mergeable");return e.isDraft!==s.isDraft&&n.add("draft"),
|
|
4
|
-
`),x.renameSync(r,n)}
|
|
3
|
+
`))&&n.add("ci"),e.reviewDecision!==s.reviewDecision&&n.add("review"),s.eventId&&s.eventId!==e.eventId&&n.add("comments"),s.state==="MERGED"&&e.state!=="MERGED"&&n.add("merged");for(const a of["mergeable","mergeStateStatus"])G(e[a])&&G(s[a])&&e[a]!==s[a]&&n.add("mergeable");return e.isDraft!==s.isDraft&&n.add("draft"),_.filter(a=>n.has(a))}S(Re,"changesBetween");function oe(e,s){const n={...s};for(const r of["mergeable","mergeStateStatus"])!G(s[r])&&G(e?.[r])&&(n[r]=e[r]);return n}S(oe,"carry");function Oe(e,s){const n=U.join(e,"data","checked.json"),r=`${n}.${process.pid}.tmp`;x.mkdirSync(U.dirname(n),{recursive:!0}),x.writeFileSync(r,JSON.stringify(s?{at:new Date().toISOString(),every:s}:{at:new Date().toISOString(),every:null,idle:!0},null,2)+`
|
|
4
|
+
`),x.renameSync(r,n)}S(Oe,"writeChecked");async function Ue({every:e,manifest:s}){const n=new Date().toISOString(),r=ee(F,{prs:{}});r.prs||={},r.acknowledged||={},r.threaded||={};const a=Y(s),i=[],c=[...new Map(a.flatMap(t=>t.prs).map(t=>[M(t),{repo:t.repo,pr:t.pr}])).values()],f=[...new Set(a.flatMap(Ne))];let o=null;if(c.length||f.length)try{o=await ue(c,f)}catch(t){i.push(t.message)}const d=new Map(a.map(t=>[t.route,[]]));if(o)for(const t of a)d.set(t.route,await Pe(t,o.open,i));const p=new Map(c.map((t,g)=>[M(t),o?o.results[g]:null])),h=o?.login??null,m=S((t,g)=>JSON.stringify([M(t),g]),"fetchKey"),y=new Map,O=new Map,R=new Map;for(const t of a){t.me=t.githubLogin||h;for(const g of t.prs){const v=M(g);y.set(v,g);const N=R.get(v)||new Set;if(N.add(t.me),R.set(v,N),!t.me)continue;const C=m(g,t.me),l=O.get(C);(!l||g.since<l.since)&&O.set(C,{...g,me:t.me})}}const u={},E={},w=new Map,D=Object.fromEntries([...y.keys()].map(t=>[t,[]])),k=new Set;for(const[t,g]of O){const v=p.get(M(g));if(!v?.view){u[t]=null,v?.error&&!k.has(v.error)&&(k.add(v.error),i.push(v.error));continue}E[t]=ge(v,g);const N=Ee(E[t]);u[t]=N,w.has(M(g))||w.set(M(g),N)}const q=[],B=[],z=new Set,ie=new Map,K=new Set,ce=new Set,de=new Set;for(const t of a){const g=t.prs.map(l=>{const $=M(l),P=JSON.stringify([t.route,l.wave,l.id]),A=r.acknowledged[$]?.[P]||r.prs[$],I=u[m(l,t.me)],j=I?Re(A,I):[];return j.length&&(D[$]=_.filter(W=>D[$].includes(W)||j.includes(W))),{p:l,key:$,recipient:P,previous:A,next:I,changes:j}});for(const l of g){const $=E[m(l.p,t.me)],P=g.filter(A=>A.p.wave===l.p.wave&&A.p.id===l.p.id);$&&await Me(t,l,$,P,{state:r,merged:ie,failed:i})&&K.add(t.route),t.skipDraft&&await be(t,l,p.get(l.key)?.view,ce,i),t.autoMerge&&await Ce(t,l,p.get(l.key)?.view,de,i)}const v=g.filter(l=>l.changes.length);let N=g.every(l=>l.next),C=!0;if(v.length||d.get(t.route).length)try{const l=[...v.map(({p:$,changes:P})=>({wave:$.wave,id:$.id,pr:$.pr,changes:P})),...d.get(t.route)];q.push({route:t.route,id:await Z(t,"github-change",{prs:l})})}catch(l){i.push(`${t.route}: could not add the github-change event (${l.message})`);for(const{key:$}of v)z.add($);N=C=!1}for(const{key:l,recipient:$,previous:P,next:A,changes:I}of g)A&&(C||!I.length)&&(r.acknowledged[l]||={},r.acknowledged[l][$]=oe(P,A));if(N)try{Oe(t.dir,t.prs.length?e:null),B.push(t.route)}catch(l){i.push(`${t.route}: could not write checked.json (${l.message})`)}}const J=[];for(const[t,g]of y){const v=w.get(t);!v||z.has(t)||(D[t].length&&J.push({repo:g.repo,pr:g.pr,changes:D[t]}),r.prs[t]={...oe(r.prs[t],v),at:n},R.get(t).size===1&&delete r.acknowledged[t])}for(const t of Object.keys(r.acknowledged))y.has(t)||delete r.acknowledged[t];for(const t of Object.keys(r.threaded))y.has(t)||delete r.threaded[t];await De(a,r,i),r.lastCheck={at:n,watched:y.size,failed:i.length,changed:J.length,boards:B},te(F,r);const L={moved:[],failed:[]};try{await Ie(a,u,m,L)}catch(t){L.failed.push(`linear: ${t.message}`)}return{at:n,watched:y.size,failed:i,changes:J,events:q,checked:B,merged:[...K],linear:L,rate:o?.rate??null}}S(Ue,"checkGitHub");const Ne=S(e=>e.github?[e.github]:[...new Set(e.waves.map(s=>s.wave.repo).filter(Boolean))],"repoList");async function Pe(e,s,n){const r=[];for(const{slug:a,wave:i}of e.waves){const c=e.github||i.repo,f=new Map((s[c]||[]).filter(o=>!o.isCrossRepository).map(o=>[o.headRefName,o.number]));for(const o of i.items){if(!f.has(o.id)||o.lane==="done"||ke(o,c).some(h=>h.repo===c))continue;const d=`${a}/${o.id}`,p=f.get(o.id);try{await se(e.dir,d,{pr:p}),await H(e.dir,d,{text:`Found PR #${p} on branch ${o.id}.`}),r.push({wave:a,id:o.id,pr:p,changes:["linked"]})}catch(h){n.push(`${e.route}: could not link PR #${p} to ${d} (${h.message})`)}}}return r}S(Pe,"linkHandPRs");function Ae(e){const s=(e.body||"").trim();return[e.kind==="review"&&{APPROVED:"Approved.",CHANGES_REQUESTED:"Changes requested."}[e.state],s].filter(Boolean).join(" ")}S(Ae,"lineText");async function Me(e,{p:s,key:n,recipient:r},a,i,{state:c,merged:f,failed:o}){const d=`${s.wave}/${s.id}`,p=new Set(c.threaded[n]?.[r]||[]),h=e.waves.find(u=>u.slug===s.wave)?.wave,m={...e.reviewerLogins,...we(h?.reviewers)};for(const u of a.events){const E=`${u.kind}:${u.id}`;if(!["user","reviewer"].includes(u.who)||u.at<=s.since||p.has(E))continue;const w=Ae(u);try{w&&await H(e.dir,d,{text:w,via:"github",who:u.who==="user"?"love":m[String(u.login).toLowerCase()]||u.login,link:u.url||`https://github.com/${s.repo}/pull/${s.pr}`}),p.add(E)}catch(D){o.push(`${e.route}: could not write ${d}'s GitHub line (${D.message})`)}}p.size&&((c.threaded[n]||={})[r]=[...p]);const y=i.map(u=>u.next);if(!ne.includes(a.state)||!y.every(u=>ne.includes(u?.state)))return!1;const O=re(y),R=JSON.stringify([e.kind==="waves"?X:e.dir,s.wave,s.id]);if(!f.has(R)){const u=e.github||e.waves.find(k=>k.slug===s.wave)?.wave.repo,E=i.filter(k=>k.next.state==="MERGED").map(k=>({repo:k.p.repo,number:k.p.pr})),w=i.map(k=>({repo:k.p.repo,number:k.p.pr})),D=O?ye(E,u):`Closed: ${Se(w,u)} closed unmerged.`;try{await se(e.dir,d,{merged:E,force:!0,text:D}),f.set(R,O)}catch(k){o.push(`${e.route}: could not record ${d} ${O?"merged":"closed"} (${k.message})`),f.set(R,!1)}}return f.get(R)}S(Me,"recordOnCard");async function be(e,{p:s,key:n},r,a,i){if(a.has(n)||r?.state!=="OPEN"||!r.isDraft||!V(r.statusCheckRollup))return;a.add(n);const c={call:"ready-draft",key:`${s.wave}/${s.id}`,reason:`The board skips the draft and PR #${s.pr}'s CI passed.`,inputs:{repo:s.repo,pr:s.pr,isDraft:r.isDraft,checks:"passed"}};try{await me({repo:s.repo,pr:s.pr})}catch(f){b(T(e.dir),{...c,result:"failed",reason:`${c.reason} gh failed: ${f.message}`},{once:!0}),i.push(`${e.route}: could not mark ${n} ready for review (${f.message})`);return}b(T(e.dir),{...c,result:"marked ready"});try{await H(e.dir,`${s.wave}/${s.id}`,{text:`CI passed, so PR #${s.pr} is marked ready for review.`})}catch(f){i.push(`${e.route}: could not write ${s.wave}/${s.id}'s ready line (${f.message})`)}}S(be,"readyGreenDraft");async function Ce(e,{p:s},n,r,a){const i=`${s.wave}/${s.id}`,c=JSON.stringify([e.dir,i]);if(r.has(c)||n?.state!=="OPEN"||n.isDraft||n.reviewDecision!=="APPROVED"||n.mergeStateStatus!=="CLEAN"||!V(n.statusCheckRollup))return;r.add(c);const f={call:"auto-merge",key:i,reason:`Auto-merge is on and PR #${s.pr} is approved, green and clean.`,inputs:{repo:s.repo,pr:s.pr,reviewDecision:n.reviewDecision,mergeStateStatus:n.mergeStateStatus,checks:"passed"}};try{await ve(e.dir,i),b(T(e.dir),{...f,result:"merged"})}catch(o){b(T(e.dir),{...f,result:"not merged",reason:`${f.reason} ${o.message}`},{once:!0}),o.message.startsWith("Not merged:")||a.push(`${e.route}: could not merge ${i} (${o.message})`)}}S(Ce,"mergeApproved");async function Ie(e,s,n,r){const a=fe();if(!a)return;const i=new Map;for(const c of e){const f=Map.groupBy(c.prs,d=>JSON.stringify([d.wave,d.id])),o=c.kind==="waves"?X:c.dir;for(const{slug:d,wave:p}of c.waves)for(const h of p.items){const m=new Set(h.linear||[]);c.tickets?.source==="linear"&&h.ticket&&m.add(h.ticket);const y=(f.get(JSON.stringify([d,h.id]))||[]).map(w=>s[n(w,c.me)]||null),O=y[$e(y)],R=h.lane==="done"||re(y),u={merged:R,ready:O?.state==="OPEN"&&!O.isDraft,hasPR:y.length>0&&(R||y.some(w=>w?.state==="OPEN")),failed:y.some(w=>!w)},E=JSON.stringify([o,d,h.id]);for(const w of m){i.has(w)||i.set(w,{items:new Map,scoped:!1,created:!1,cards:[]});const D=i.get(w),k=D.items.get(E);D.items.set(E,{...u,failed:u.failed||k?.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 i){const o=[...f.items.values()];if(!f.scoped||o.some(m=>m.failed))continue;const d=o.every(m=>m.merged)?"done":o.every(m=>m.merged||m.ready)?"review":o.some(m=>m.hasPR)?"started":null;if(!d)continue;const p={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:p,inputs:{target:d,cards:f.cards,items:o}};try{const m=await a.advanceIssue(c,d,{owner:a.saved.viewer.id,created:f.created});m&&(r.moved.push({id:c,state:m}),b(ae(),{...h,result:`moved to ${m}`}))}catch(m){b(ae(),{...h,result:"failed",reason:`${p} ${m.message}`},{once:!0}),r.failed.push(`linear ${c}: ${m.message}`)}}}S(Ie,"advanceLinear");async function Ve({now:e=new Date,settings:s,manifest:n}){const r=ee(Q,{});r.sent||={};const a=pe(e,s,r.sent),i=[],c=[],f=[];if(!a.length)return{due:a,events:i,failed:c};const o=Y(n);for(const d of o)try{i.push({route:d.route,id:await Z(d,"check-github",{reason:"morning"})})}catch(p){c.push(`${d.route}: could not add the morning check-github event (${p.message})`),f.push(d.route)}if(i.length||!o.length){for(const d of a)r.sent[d]=he(e);r.last={at:e.toISOString(),due:a,routes:i.map(d=>d.route),failed:f},te(Q,r)}return{due:a,events:i,failed:c}}S(Ve,"sendWakes");export{_ as CHANGE_KINDS,oe as carry,Re as changesBetween,Ue as checkGitHub,Ve as sendWakes,Ee as summarize};
|