@lovek4d/kickflip 0.7.0 → 0.7.2
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 +53 -49
- package/SKILL.md +17 -13
- package/bin/event-lines.mjs +1 -1
- package/bin/kickflip.mjs +3 -3
- package/board/agents.mjs +2 -2
- package/board/arch-review.mjs +4 -4
- package/board/bin/board.mjs +113 -105
- package/board/domain.mjs +7 -7
- package/board/finish.mjs +4 -4
- package/board/headless.mjs +8 -8
- package/board/hub-lines.mjs +1 -0
- package/board/onboarding.mjs +3 -3
- package/board/public/app-arch.js +1 -1
- package/board/public/app-cards.js +6 -7
- package/board/public/app-header.js +1 -1
- package/board/public/app-lanes.js +39 -62
- package/board/public/app-needs.js +16 -11
- package/board/public/app-reader.js +9 -5
- package/board/public/app.js +3 -3
- package/board/public/attach.js +18 -2
- package/board/public/dialogs.js +11 -13
- package/board/public/hub-kit.js +1 -1
- package/board/public/hub.css +0 -1
- package/board/public/linear-key.js +2 -2
- package/board/public/new-task.js +1 -1
- package/board/public/prototype-kit.js +2 -5
- package/board/public/router.js +1 -1
- package/board/public/sidebar.css +3 -3
- package/board/public/sidebar.js +2 -2
- package/board/public/style.css +43 -43
- package/board/public/ui-cards.js +25 -20
- package/board/public/ui-hub.js +1 -5
- package/board/public/ui-links.js +5 -5
- package/board/public/ui-order.js +6 -7
- package/board/public/ui-questions.js +17 -13
- package/board/public/ui-sidebar.js +1 -1
- package/board/public/ui-threads.js +29 -21
- package/board/public/ui.js +23 -6
- package/board/public/view-hub-settings.js +11 -19
- package/board/public/view-hub.js +25 -11
- package/board/public/view-notes.js +1 -1
- package/board/public/view-questions.js +18 -5
- package/board/public/view-settings.js +7 -7
- package/board/render-check.mjs +10 -10
- package/board/review.mjs +1 -1
- package/board/server.mjs +18 -19
- package/board/stop.mjs +1 -1
- package/board/store.mjs +26 -26
- package/board/tickets.mjs +3 -3
- package/hooks-plugin/hooks/board-data-gate.mjs +6 -6
- package/hub/awake.mjs +1 -1
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/github.mjs +3 -3
- package/hub/hub.mjs +11 -11
- package/hub/incoming.mjs +2 -2
- package/hub/linear.mjs +1 -1
- package/hub/locks.mjs +1 -1
- package/hub/new-board.mjs +2 -2
- package/hub/owner-checks.mjs +1 -1
- package/hub/readiness.mjs +1 -1
- package/hub/review-send.mjs +1 -1
- package/hub/slack.mjs +1 -1
- package/hub/try.mjs +6 -7
- package/hub/views.mjs +1 -1
- package/package.json +1 -1
- package/shared/usage-gate.mjs +1 -1
- package/starter/server.mjs +3 -3
package/board/tickets.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
2
|
-
`)[0]);let
|
|
3
|
-
`)[0]})`,"why");try{if(
|
|
1
|
+
var x=Object.defineProperty;var r=(t,e)=>x(t,"name",{value:e,configurable:!0});import{runRepo as k,validateGithub as O}from"../hub/repos.mjs";import{savedClient as $}from"../hub/linear.mjs";const i=r(t=>{throw Object.assign(new Error(t),{status:400})},"refuse");function I(t){(!t||typeof t!="object"||Array.isArray(t))&&i("Tickets must name a source.");const e={none:["source"],github:["source","account"],linear:["source","workspace","team","project"]};return Object.hasOwn(e,t.source)||i("Ticket source must be none, github or linear."),Object.keys(t).some(n=>!e[t.source].includes(n))&&i("Unknown ticket source field."),t.source==="none"?null:t.source==="github"?t.account===void 0||t.account===""?{source:"github"}:((typeof t.account!="string"||!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/.test(t.account))&&i("The gh account must be a GitHub login."),{source:"github",account:t.account}):((typeof t.workspace!="string"||!/^[a-z\d][a-z\d-]*$/i.test(t.workspace))&&i("Linear needs a workspace."),(typeof t.team!="string"||!/^[A-Z][A-Z0-9]*$/.test(t.team))&&i("Linear needs an uppercase team key."),(typeof t.project!="string"||!t.project.trim())&&i("Linear needs a project."),{source:"linear",workspace:t.workspace,team:t.team,project:t.project.trim()})}r(I,"ticketSource");const b=r(t=>{const e=t?Number(t[1]):null;return Number.isSafeInteger(e)?e:null},"numberOf"),y=r(t=>b(/^#?([1-9]\d*)$/.exec(String(t??""))),"issueNumber"),S=r(t=>t.source==="linear"?t.team:null,"keyOf");function p(t,e){if(!t)return null;const n=S(t),s=String(e??"");return n?s.startsWith(`${n}-`)?b(/^([1-9]\d*)$/.exec(s.slice(n.length+1))):null:b(/^#([1-9]\d*)$/.exec(s))}r(p,"ticketIn");function N(t,e,{repo:n}={}){const s=p(t,e);if(!s)return null;if(t.source==="github")return n?{id:`#${s}`,url:`https://github.com/${n}/issues/${s}`,source:"github"}:null;const c=`${t.team}-${s}`;return{id:c,url:`https://linear.app/${t.workspace}/issue/${c}`,source:"linear"}}r(N,"ticketBadge");async function j(t,e){if(!t.account)return e;const n=await e("gh",["auth","token","-u",t.account]);(n.status!==0||!n.stdout?.trim())&&i(`gh has no ${t.account} account; run gh auth login`);const s={...process.env,GH_TOKEN:n.stdout.trim()};return(c,g)=>e(c,g,{env:s})}r(j,"issueRunner");const L=r(async t=>{try{return await t()}catch(e){i(e.message)}},"linearCall");async function E(t,{ticket:e,title:n,body:s="",repo:c},{run:g=k,linearFetch:o}={}){if(t.source==="linear"){e!==void 0&&!p(t,e)&&i(`Ticket must match ${t.team}-<number>.`);const a=$({fetch:o});if(a||i("Hub has no Linear key, so this board can\u2019t create Linear tickets; add it in Hub \u203A Integrations \u203A Linear, or pick another ticket source."),e===void 0)return{ticket:(await L(()=>a.createIssue({team:t.team,project:t.project,title:n,description:s}))).identifier,created:!0};const d=await L(()=>a.issue(e));return d||i(`Linear has no issue ${e}.`),d.team!==t.team&&i(`${e} is in Linear's ${d.team} team, not ${t.team}.`),{ticket:d.identifier,created:!1}}O(c);const f=e===void 0?null:y(e);e!==void 0&&!f&&i("GitHub ticket must be an issue number such as #41.");const h=f?["issue","view",String(f),"--repo",c,"--json","number,url"]:["issue","create","--repo",c,"--title",n,"--body",s],l=await(await j(t,g))("gh",h);l.status!==0&&i((l.stderr||"GitHub refused the issue.").trim().split(`
|
|
2
|
+
`)[0]);let m;if(f){let a;try{a=JSON.parse(l.stdout)}catch{i("GitHub returned an invalid issue.")}a.number!==f&&i("GitHub returned a different issue."),m=a.url}else m=l.stdout?.trim();const u=/^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/([1-9]\d*)$/.exec(m||"");return(!u||u[1].toLowerCase()!==c.toLowerCase()||f&&Number(u[2])!==f||!y(u[2]))&&i("Ticket must be an issue in the board\u2019s repo."),{ticket:`#${u[2]}`,created:!f}}r(E,"resolveTicket");const C=r(t=>t.startsWith("#")?`GH-${t.slice(1)}`:t,"cardIdOf");function Z(t,e){const n=t.source==="github"?String(e??"").match(/(?<![\w#])#[1-9]\d*\b/g):String(e??"").match(/\b[A-Z][A-Z0-9]*-[1-9]\d*\b/g);return[...new Set((n||[]).filter(s=>p(t,s)))]}r(Z,"titleTickets");function R({start:t,done:e,handover:n}){return e?"done":t?"started":n?"review":null}r(R,"ticketTarget");const T={started:"In Progress",review:"In Review",done:"Done"};async function z(t,e,n,{repo:s,run:c=k,linearFetch:g}={}){const o=e.ticket;if(!t||!n||!o||!p(t,o)||t.source==="github"&&n!=="done")return null;if(t.source==="github"&&!s)return`ticket ${o}: not closed, the board has no GitHub repo`;const f=r(h=>`ticket ${o}: could not move to ${T[n]} (${String(h.message||h).split(`
|
|
3
|
+
`)[0]})`,"why");try{if(t.source==="linear"){const u=$({fetch:g});if(!u)return`ticket ${o}: not moved, Hub has no Linear key`;if(!e.ticket_created){const d=await u.issue(o);if(!d)return`ticket ${o}: not moved, Linear has no such issue`;if(d.assignee!==u.saved.viewer?.id)return`ticket ${o}: left as it is, not assigned to you`}const a=await u.moveIssue(o,n);return a?`ticket ${o}: moved to ${a}`:null}const h=o.slice(1),l=await j(t,c);if(!e.ticket_created){const u=await l("gh",["issue","view",h,"--repo",s,"--json","state,assignees"]);if(u.status!==0)throw new Error(u.stderr||"gh issue view failed");const a=await l("gh",["api","user","--jq",".login"]);if(a.status!==0)throw new Error(a.stderr||"gh api user failed");const d=JSON.parse(u.stdout);if(d.state!=="OPEN")return null;if(!(d.assignees||[]).some(w=>w.login?.toLowerCase()===a.stdout.trim().toLowerCase()))return`ticket ${o}: left as it is, not assigned to you`}const m=await l("gh",["issue","close",h,"--repo",s]);if(m.status!==0)throw new Error(m.stderr||"gh issue close failed");return`ticket ${o}: closed`}catch(h){return f(h)}}r(z,"moveTicket");export{C as cardIdOf,z as moveTicket,E as resolveTicket,N as ticketBadge,p as ticketIn,I as ticketSource,R as ticketTarget,Z as titleTickets};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// A PreToolUse hook on Edit|Write that refuses a hand edit of the files board.mjs owns on a board site: the site's
|
|
3
|
-
// board.json, every .json and .md under its data/, the
|
|
4
|
-
// a ~/.claude/waves
|
|
3
|
+
// board.json, every .json and .md under its data/, the item file(s) board.json's `items` names, and the .md notes beside
|
|
4
|
+
// a ~/.claude/waves item file. It denies, not asks, because a headless session has nobody to ask, and the reason names
|
|
5
5
|
// the board.mjs command that makes the change. Drafts (<site>/drafts/, or any folder outside data/) and Bash writes stay
|
|
6
6
|
// open (docs/adr/2026-11-05-2330-a-hook-refuses-hand-edits-to-board-data-files.md,
|
|
7
7
|
// docs/adr/2026-11-09-1100-the-board-owns-a-cards-notes-file.md).
|
|
@@ -15,22 +15,22 @@ process.stdout.on('error', () => {});
|
|
|
15
15
|
const real = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
|
|
16
16
|
const inside = (dir, file) => !path.relative(dir, file).startsWith('..') && !path.isAbsolute(path.relative(dir, file));
|
|
17
17
|
|
|
18
|
-
// → the file's kind on this board ('
|
|
18
|
+
// → the file's kind on this board ('file', 'board', 'notes' or 'data'), or null when it is not board-owned
|
|
19
19
|
function ownedKind(board, file) {
|
|
20
20
|
const dir = real(board.dir);
|
|
21
21
|
let items = 'waves';
|
|
22
22
|
try { items = JSON.parse(fs.readFileSync(path.join(dir, 'board.json'), 'utf8')).items || 'waves'; } catch {}
|
|
23
23
|
if (file === path.join(dir, 'board.json')) return 'board';
|
|
24
|
-
if (items === 'waves' ? path.basename(file) === 'wave.json' && inside(real(path.join(os.homedir(), '.claude', 'waves')), file) : file === real(path.resolve(dir, items))) return '
|
|
24
|
+
if (items === 'waves' ? path.basename(file) === 'wave.json' && inside(real(path.join(os.homedir(), '.claude', 'waves')), file) : file === real(path.resolve(dir, items))) return 'file';
|
|
25
25
|
if (file.endsWith('.md') && (inside(path.join(dir, 'data'), file) || (items === 'waves' && inside(real(path.join(os.homedir(), '.claude', 'waves')), file)))) return 'notes';
|
|
26
26
|
if (file.endsWith('.json') && inside(path.join(dir, 'data'), file)) return 'data';
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const HINTS = {
|
|
31
|
-
|
|
31
|
+
file: 'A card changes with `board.mjs --site <route> item add|set <file>/<id> …`',
|
|
32
32
|
board: 'Board settings change with `board.mjs --site <route> rules|reviewers …` or the settings page',
|
|
33
|
-
notes: 'A card\'s notes change with `board.mjs --site <route> item notes <
|
|
33
|
+
notes: 'A card\'s notes change with `board.mjs --site <route> item notes <file>/<id> --heading "<heading>" --file <draft.md>`, the draft written outside data/',
|
|
34
34
|
data: 'Board data changes with a board.mjs command (`item set`, `thread`, `topic add`, `question add --file <draft>`, `reply`, `builder …`); its usage text lists them',
|
|
35
35
|
};
|
|
36
36
|
|
package/hub/awake.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var l=Object.defineProperty;var n=(e,r)=>l(e,"name",{value:r,configurable:!0});import{spawn as w}from"node:child_process";import{readSettingsFile as f,writeSettingsFile as c}from"./push.mjs";const d={linux:"Linux",win32:"Windows",freebsd:"FreeBSD"},k=n((e=process.platform)=>e==="darwin"?null:`macOS only \xB7 this host runs ${d[e]||e}`,"awakeUnsupported"),_=n((e=f())=>e.keep_awake===!0,"awakeOn");function g(e,{platform:r=process.platform}={}){const t=k(r);if(t)throw Object.assign(new Error(`Keep awake runs caffeinate, ${t}.`),{status:409});const{keep_awake:s,keep_awake_until:o,...a}=f();c(e?{...a,keep_awake:!0}:a)}n(g,"setKeepAwake");function $({log:e=n(()=>{},"log"),platform:r=process.platform}={}){let t=null;const s=n(()=>{t&&(e(`keep awake: stopping caffeinate (pid ${t.pid})`),t.kill("SIGTERM"),t=null)},"end");function o(){const a=f();if("keep_awake_until"in a){const{keep_awake_until:p,...u}=a;return c(p==="forever"?{...u,keep_awake:!0}:u),o()}if(k(r)||!_(a)){s();return}if(t)return;const i=w("caffeinate",["-s","-w",String(process.pid)],{stdio:"ignore"});i.on("error",p=>{e(`keep awake: caffeinate failed: ${p.message}`),t===i&&(t=null)}),i.on("exit",()=>{t===i&&(t=null)}),t=i,e(`keep awake: caffeinate -s (pid ${i.pid}) until turned off`)}return n(o,"apply"),{apply:o,stop:s}}n($,"caffeinate");export{_ as awakeOn,k as awakeUnsupported,$ as caffeinate,g as setKeepAwake};
|
package/hub/board-repo.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
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
|
|
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 L,runRepo as O,githubFromOrigin as j,defaultBranch as M}from"./repos.mjs";import{repoInput as D}from"./new-board.mjs";import{boardConfig as F}from"../board/store.mjs";import{storedPath as p}from"../bin/install.mjs";import{cloneRoot as I}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 K=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}=D(r),s=L(r.branch),u=m(h().sites,e).dir;t&&(await F(u)).tickets?.source==="github"&&w("This board files its cards as GitHub issues. Switch Tickets to None 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 M(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=I()}={}){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,K as mainRefOf,U as migrateBoardRepos,V as migrateHubBoard};
|
package/hub/boards.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var q=Object.defineProperty;var i=(e,t)=>q(e,"name",{value:t,configurable:!0});import
|
|
2
|
-
`),
|
|
3
|
-
`),s.id}i(
|
|
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=
|
|
1
|
+
var q=Object.defineProperty;var i=(e,t)=>q(e,"name",{value:t,configurable:!0});import p 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 S,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 v}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 fe}from"../board/review.mjs";import{cardPRs as pe}from"../shared/card-prs.mjs";const de=c.resolve(c.dirname(Q(import.meta.url)),".."),F=c.join(z.homedir(),".claude"),m=c.join(F,"kickflip"),C=c.join(F,"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"),ge=c.join(m,"repo-fetch.json"),M=c.join(m,"hub-code.json"),we="1970-01-01T00:00:00Z",ke=2*6e4,je=15*6e4,f=i((e,t)=>{try{return JSON.parse(p.readFileSync(e,"utf8"))}catch{return t}},"readJSON");function k(e,t){const n=`${e}.${process.pid}.tmp`;p.mkdirSync(c.dirname(e),{recursive:!0}),p.writeFileSync(n,JSON.stringify(t,null,2)+`
|
|
2
|
+
`),p.renameSync(n,e)}i(k,"saveJSON");const j=i(()=>f($,{sites:{}}),"readManifest"),lt=i(e=>re($,e),"withManifestLock");function ft(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=v(o.dir)),Array.isArray(o.cmd)&&(o.cmd=o.cmd.map(l=>v(l))),o.owner?.repo&&(o.owner.repo=v(o.owner.repo));for(const o of Object.values(e.starting||{}))o.repo&&(o.repo=v(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(ft,"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),"isItemFile"),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 Se(e,t,n){if(n==="file"){const s=f(c.resolve(e,t.items),null);return H(s)?[{slug:t.slug||c.basename(t.items,".json"),itemFile:s}]:[]}let r=[];try{r=p.readdirSync(C,{withFileTypes:!0}).filter(s=>s.isDirectory()).map(s=>s.name).sort()}catch{}return r.map(s=>({slug:s,itemFile:f(c.join(C,s,"wave.json"),null)})).filter(s=>H(s.itemFile))}i(Se,"filesOf");function ve(e,t){return e.flatMap(({slug:n,itemFile:r})=>r.items.filter(s=>s&&s.lane!=="done").flatMap(s=>pe(s,t||r.repo).map(a=>({file:n,id:s.id,pr:a.number,repo:a.repo,since:s.gh_since||we}))))}i(ve,"prsOf");function T(e,t){const n=f(c.join(t.dir,"board.json"),{}),r=typeof n.items=="string"&&n.items!=="waves"?"file":"waves",s=Se(t.dir,n,r),a=t.github;return{route:e,dir:t.dir,port:t.port,title:n.title||t.title||e,kind:r,files:s,prs:ve(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 pt(e=j()){const t=Object.entries(e?.sites||{}).find(([,n])=>n?.hub===!0&&A(n));return t?T(...t):void 0}i(pt,"hubBoard");function Oe(e,t){const n=i(r=>{try{return p.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 G(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 p.mkdirSync(c.join(e.dir,"data"),{recursive:!0}),p.appendFileSync(c.join(e.dir,"data","events.jsonl"),JSON.stringify(s)+`
|
|
3
|
+
`),s.id}i(G,"appendEvent");async function ht(e,t,n){const r=[],s=[];for(const a of e)try{r.push({route:a.route,id:await G(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=f(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 K(r.pid)?.includes("wait-events.mjs")??!0}i($e,"watcherRunning");const K=i(e=>ne(["-p",String(e),"-o","command="]),"commandOf"),W=i(e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"escaped");function mt(e,t,n,{command:r=K,kill:s=process.kill}={}){const a=f(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+['"]?${W(t)}['"]?\\s+['"]?${W(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=S()}={}){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=S(),wakes:r=f(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 gt(e){const t=e.owner?.session?f(O(e),{}):{};return Object.fromEntries(e.watch.map(n=>{const r=d(t,n);return[n,Number.isSafeInteger(r)&&r>=0?r:0]}))}i(gt,"watcherCursor");const wt=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=f(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=p.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=f(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=S(),wakes:r=f(E,{})}={}){if(!n)return{pushed:[],failed:[]};const s=Object.assign(Object.create(null),f(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 g=xe(u);if(!g||t-Date.parse(g)<je)continue;const y=Math.round((t-Date.parse(g))/6e4),w=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 ${w}`,url:`/${u.route}/`,tag:`deaf-${u.route}`},{kind:"fault",immediate:!0,at:t}),s[u.route]={at:t.toISOString(),since:g},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&&!p.existsSync(O(n))&&k(O(n),Object.fromEntries(Object.entries(D(n)).map(([s,a])=>[s,a.length])));const r={id:await G(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=f(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{itemFile:u}=e.files.find(w=>w.slug===o.file),h=u.items.find(w=>w.id===o.id),g=le(u,h,`${o.file}/${o.id}`,n,ue(r,[{repo:o.repo,number:o.pr}])),y=[...new Set([...e.reviewers,...U(u.reviewers)])].filter(w=>!Object.hasOwn(g,w));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=p.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 St=i(e=>I(c.resolve(e.cwd||e.dir,e.cmd.find(t=>J.test(t)))).hash,"boardCode");function vt(){const e={...I(),pid:process.pid,at:new Date().toISOString()};return k(M,e),e}i(vt,"recordHubCode");const Fe=ce().restartCommand,Ce=i(()=>f(M,null)?.hash??null,"runningHubCode"),Pe=i(()=>({running:Ce(),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 ${Fe}`},"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(f(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 g=Me[u.link](te);return{...u,state:n-Date.parse(h)>=g?"broken":"settling",since:h}});if(s&&JSON.stringify(o)!==JSON.stringify(a)){const u=Object.assign(Object.create(null),f(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??f(he,{}),r=t.settings??ee(),s=[Ae(e,n),Te(d(t.fetches??f(ge,{}),e.route)),Le(t.listen??L(e)),Re(e,r,t.wakes??f(me,{})),De(e,n,t.topics??fe())].filter(Boolean);return Be(e.route,s,{now:t.now??Date.now(),faults:t.faults})}i(Ue,"linksOf");function $t(e){const t={running:S(),wakes:f(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,ge as REPO_FETCH,E as RESUME_STATE,me as WAKE_STATE,C as WAVES,G as appendEvent,dt as boardAt,St as boardCode,ye as buildsLocally,kt as carryCursor,jt as checkListening,bt as handshake,_e as handshakeFile,pt 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,ve as prsOf,f as readJSON,j as readManifest,vt as recordHubCode,$t as recordLinkFaults,be as reviewerLogins,Ce as runningHubCode,Oe as samePath,k as saveJSON,ht as sendToBoards,mt as stopPreviousWatcher,wt as watchedCounts,D as watchedLines,gt as watcherCursor,$e as watcherRunning,lt as withManifestLock,ft as writeManifest};
|
package/hub/github.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var le=Object.defineProperty;var
|
|
1
|
+
var le=Object.defineProperty;var v=(e,s)=>le(e,"name",{value:s,configurable:!0});import F from"node:fs";import W from"node:path";import"node:os";import{savedClient as fe}from"./linear.mjs";import{checksPassed as x,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 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,commits:Array.isArray(e.commits)?e.commits:[]}}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
|
-
`))&&
|
|
4
|
-
`),
|
|
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=W.join(e,"data","checked.json"),n=`${r}.${process.pid}.tmp`;F.mkdirSync(W.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
|
+
`),F.renameSync(n,r)}v(Re,"writeChecked");async function xe({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 p=new Map(c.map((t,g)=>[P(t),o?o.results[g]:null])),h=o?.login??null,u=v((t,g)=>JSON.stringify([P(t),g]),"fetchKey"),y=new Map,R=new Map,E=new Map;for(const t of i){t.me=t.githubLogin||h;for(const g of t.prs){const k=P(g);y.set(k,g);const N=E.get(k)||new Set;if(N.add(t.me),E.set(k,N),!t.me)continue;const C=u(g,t.me),l=R.get(C);(!l||g.since<l.since)&&R.set(C,{...g,me:t.me})}}const m={},O={},w=new Map,D=Object.fromEntries([...y.keys()].map(t=>[t,[]])),S=new Set;for(const[t,g]of R){const k=p.get(P(g));if(!k?.view){m[t]=null,k?.error&&!S.has(k.error)&&(S.add(k.error),a.push(k.error));continue}O[t]=ge(k,g);const N=Ee(O[t]);m[t]=N,w.has(P(g))||w.set(P(g),N)}const _=[],B=[],q=new Set,ae=new Map,z=new Set,ce=new Set,de=new Set;for(const t of i){const g=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(K=>D[$].includes(K)||L.includes(K))),{p:l,key:$,recipient:A,previous:M,next:I,changes:L}});for(const l of g){const $=O[u(l.p,t.me)],A=g.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})&&z.add(t.route),t.skipDraft&&await be(t,l,p.get(l.key)?.view,ce,a),t.autoMerge&&await Ce(t,l,p.get(l.key)?.view,de,a)}const k=g.filter(l=>l.changes.length);let N=g.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 g)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,g]of y){const k=w.get(t);!k||q.has(t)||(D[t].length&&J.push({repo:g.repo,pr:g.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:[...z],linear:j,rate:o?.rate??null}}v(xe,"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}`,p=f.get(o.id);try{await se(e.dir,d,{pr:p}),await te(e.dir,d,"pr-found",{pr:p,branch:o.id}),n.push({file:i,id:o.id,pr:p,changes:["linked"]})}catch(h){r.push(`${e.route}: could not link PR #${p} 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}`,p=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||p.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}`}),p.add(O)}catch(D){o.push(`${e.route}: could not write ${d}'s GitHub line (${D.message})`)}}p.size&&((c.threaded[r]||={})[n]=[...p]);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||!x(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"||!x(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:p}of c.files)for(const h of p.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 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 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:`${p} ${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=pe(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(p){c.push(`${d.route}: could not add the morning check-github event (${p.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,xe as checkGitHub,Ue as sendWakes,Ee as summarize};
|