@gredescerber/agent-crew 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/board.js +4 -1
- package/src/commands/digest.js +11 -3
- package/src/core/board.js +19 -5
package/package.json
CHANGED
package/src/commands/board.js
CHANGED
|
@@ -9,6 +9,7 @@ import { gh, ghJson } from '../core/exec.js';
|
|
|
9
9
|
import { loadConfig } from '../core/config.js';
|
|
10
10
|
import { listIssues } from '../core/issues.js';
|
|
11
11
|
import { boardModel, actionArgs, renderPage } from '../core/board.js';
|
|
12
|
+
import { openQuestions, teamOf } from './digest.js';
|
|
12
13
|
|
|
13
14
|
const PR_FIELDS = 'number,title,body,headRefOid,createdAt,isDraft,author,labels,comments,statusCheckRollup,mergeable,isCrossRepository,autoMergeRequest';
|
|
14
15
|
|
|
@@ -19,6 +20,8 @@ export function githubSource(root) {
|
|
|
19
20
|
prs: () => ghJson(['pr', 'list', '--state', 'open', '--limit', '50', '--json', PR_FIELDS], { cwd: root, allowFail: true }) ?? [],
|
|
20
21
|
members: () => (gh(['api', 'repos/{owner}/{repo}/collaborators', '--paginate', '--jq', '.[].login'], { cwd: root, allowFail: true }).stdout || '').split('\n').filter(Boolean),
|
|
21
22
|
edit: (args) => gh(args, { cwd: root, allowFail: true }),
|
|
23
|
+
questions: () => openQuestions(root),
|
|
24
|
+
team: () => teamOf(root),
|
|
22
25
|
};
|
|
23
26
|
}
|
|
24
27
|
|
|
@@ -41,7 +44,7 @@ export function boardHandler({ source, token, repo, config = {}, allowedHosts })
|
|
|
41
44
|
if (!url.pathname.startsWith('/api/')) return json(res, 404, { error: 'not found' });
|
|
42
45
|
if (req.headers['x-crew-token'] !== token) return json(res, 403, { error: 'bad token — reload the page from the URL crew board printed' });
|
|
43
46
|
if (req.method === 'GET' && url.pathname === '/api/board') {
|
|
44
|
-
return json(res, 200, boardModel({ issues: source.issues(), prs: source.prs(), members: getMembers(), config }));
|
|
47
|
+
return json(res, 200, boardModel({ issues: source.issues(), prs: source.prs(), members: getMembers(), questions: source.questions?.() ?? [], team: source.team?.() ?? null, config }));
|
|
45
48
|
}
|
|
46
49
|
if (req.method === 'POST' && url.pathname === '/api/action') {
|
|
47
50
|
if (!String(req.headers['content-type'] ?? '').startsWith('application/json')) return json(res, 415, { error: 'json only' });
|
package/src/commands/digest.js
CHANGED
|
@@ -13,14 +13,22 @@ import { digestModel, renderDigest, DIGEST_LABEL, DIGEST_TITLE } from '../core/d
|
|
|
13
13
|
|
|
14
14
|
const PR_FIELDS = 'number,title,body,headRefOid,createdAt,isDraft,author,labels,comments,statusCheckRollup,mergeable,isCrossRepository,autoMergeRequest';
|
|
15
15
|
|
|
16
|
+
export const openQuestions = (root) =>
|
|
17
|
+
ghJson(['issue', 'list', '--state', 'open', '--label', QUESTION_LABEL, '--limit', '100', '--json', 'number,title,body,labels,assignees,createdAt'], { cwd: root, allowFail: true }) ?? [];
|
|
18
|
+
|
|
19
|
+
/** The crew-team block of docs/prd.md in this checkout, or null. */
|
|
20
|
+
export function teamOf(root) {
|
|
21
|
+
const p = join(root, DEFAULT_PRD);
|
|
22
|
+
return existsSync(p) ? parsePrd(readFileSync(p, 'utf8')).team : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
/** Live data through gh (tests pass deps.data instead). */
|
|
17
26
|
export function githubData(root) {
|
|
18
|
-
const p = join(root, DEFAULT_PRD);
|
|
19
27
|
return {
|
|
20
28
|
issues: listIssues({ limit: 500 }),
|
|
21
29
|
prs: ghJson(['pr', 'list', '--state', 'open', '--limit', '50', '--json', PR_FIELDS], { cwd: root, allowFail: true }) ?? [],
|
|
22
|
-
questions:
|
|
23
|
-
team:
|
|
30
|
+
questions: openQuestions(root),
|
|
31
|
+
team: teamOf(root),
|
|
24
32
|
};
|
|
25
33
|
}
|
|
26
34
|
|
package/src/core/board.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { labelValue } from './issues.js';
|
|
4
4
|
import { prQueue } from './queue.js';
|
|
5
5
|
import { LOGIN_RE } from './prd.js';
|
|
6
|
+
import { digestModel } from './digest.js';
|
|
6
7
|
|
|
7
8
|
export const COLUMNS = ['ready', 'claimed', 'review', 'waiting', 'blocked', 'needs-spec'];
|
|
8
9
|
export const PRIORITIES = ['high', 'normal', 'low'];
|
|
@@ -11,7 +12,7 @@ export const BOARD_STATUSES = ['ready', 'blocked', 'needs-spec', 'waiting'];
|
|
|
11
12
|
|
|
12
13
|
const prio = (i) => labelValue(i, 'priority') ?? 'normal';
|
|
13
14
|
|
|
14
|
-
export function boardModel({ issues = [], prs = [], members = [], config = {}, now = Date.now() } = {}) {
|
|
15
|
+
export function boardModel({ issues = [], prs = [], members = [], questions = [], team = null, config = {}, now = Date.now() } = {}) {
|
|
15
16
|
const columns = Object.fromEntries(COLUMNS.map((c) => [c, []]));
|
|
16
17
|
const other = [];
|
|
17
18
|
for (const i of issues) {
|
|
@@ -35,7 +36,9 @@ export function boardModel({ issues = [], prs = [], members = [], config = {}, n
|
|
|
35
36
|
const queue = prQueue(prs, { me: 'board', login: null, reviewers: [], config, now }).map((x) => ({
|
|
36
37
|
kind: x.kind, number: x.number, title: x.title, risk: x.risk, age_min: x.age, reason: x.reason ?? null, have: x.have ?? null, need: x.need ?? null,
|
|
37
38
|
}));
|
|
38
|
-
|
|
39
|
+
// Captain panel: the same status crew digest posts to the pinned issue.
|
|
40
|
+
const digest = digestModel({ issues, prs, questions, team, config, now });
|
|
41
|
+
return { columns, untracked: other.length, prs: queue, digest, members: [...new Set(members)].sort((a, b) => a.localeCompare(b)), generated_at: new Date(now).toISOString() };
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/**
|
|
@@ -87,20 +90,26 @@ export function renderPage({ token, repo }) {
|
|
|
87
90
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
88
91
|
<title>crew board — ${esc(repo)}</title>
|
|
89
92
|
<style>
|
|
90
|
-
:root{--bg:#f6f7f9;--card:#fff;--ink:#1d2433;--muted:#667085;--line:#e3e6ec;--hi:#d92d20}
|
|
91
|
-
@media (prefers-color-scheme:dark){:root{--bg:#111418;--card:#1a1f26;--ink:#e6e9ef;--muted:#98a2b3;--line:#2a313b}}
|
|
93
|
+
:root{--bg:#f6f7f9;--card:#fff;--ink:#1d2433;--muted:#667085;--line:#e3e6ec;--hi:#d92d20;--warn:#b54708;--good:#067647}
|
|
94
|
+
@media (prefers-color-scheme:dark){:root{--bg:#111418;--card:#1a1f26;--ink:#e6e9ef;--muted:#98a2b3;--line:#2a313b;--hi:#f97066;--warn:#fdb022;--good:#47cd89}}
|
|
92
95
|
*{box-sizing:border-box}body{margin:0;font:14px/1.4 system-ui,sans-serif;background:var(--bg);color:var(--ink)}
|
|
93
96
|
header{display:flex;gap:12px;align-items:center;padding:12px 16px;border-bottom:1px solid var(--line);flex-wrap:wrap}
|
|
94
97
|
header h1{font-size:16px;margin:0}.muted{color:var(--muted)}
|
|
95
98
|
main{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;padding:12px 16px}
|
|
96
|
-
section h2,#prs h2{font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin:4px 0 8px}
|
|
99
|
+
section h2,#prs h2,.alerts h2{font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin:4px 0 8px}
|
|
97
100
|
.card{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:10px;margin-bottom:8px}
|
|
98
101
|
.card a{color:var(--ink);text-decoration:none;font-weight:600}.meta{color:var(--muted);font-size:12px;margin:4px 0 6px}
|
|
99
102
|
.prio-high{border-left:4px solid var(--hi)}.prio-low{opacity:.75}
|
|
100
103
|
.row{display:flex;gap:6px;flex-wrap:wrap}select{font:inherit;font-size:12px;padding:2px 4px;border-radius:6px;border:1px solid var(--line);background:var(--card);color:var(--ink)}
|
|
101
104
|
#prs{padding:0 16px 16px}#prs a{color:var(--ink)}#prs li{margin:4px 0}#msg{color:var(--hi)}
|
|
105
|
+
#cap{padding:12px 16px 0}.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px}
|
|
106
|
+
.kpi{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:8px 10px}.kpi b{display:block;font-size:22px;font-variant-numeric:tabular-nums}
|
|
107
|
+
.kpi span{color:var(--muted);font-size:12px}.kpi.bad{border-color:var(--hi)}.kpi.bad b{color:var(--hi)}.kpi.warn{border-color:var(--warn)}.kpi.warn b{color:var(--warn)}
|
|
108
|
+
.alerts{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:8px;margin-top:8px}.alerts ul{margin:0;padding-left:18px}.alerts li{margin:2px 0}
|
|
109
|
+
.ok{color:var(--good)}
|
|
102
110
|
</style></head><body>
|
|
103
111
|
<header><h1>crew board</h1><span class="muted">${esc(repo)}</span><span class="muted" id="time"></span><span id="msg"></span></header>
|
|
112
|
+
<div id="cap"><div class="kpis" id="kpis"></div><div class="alerts" id="alerts"></div></div>
|
|
104
113
|
<main id="cols"></main>
|
|
105
114
|
<div id="prs"><h2>Pull requests waiting</h2><ul id="prlist"></ul></div>
|
|
106
115
|
<script>
|
|
@@ -111,6 +120,11 @@ const sel=(opts,cur,on)=>{const s=e('select',{on:{change:()=>on(s.value)}});for(
|
|
|
111
120
|
async function act(issue,op,value){const r=await fetch('/api/action',{method:'POST',headers:{'content-type':'application/json','x-crew-token':TOKEN},body:JSON.stringify({issue,op,value})});const j=await r.json();document.getElementById('msg').textContent=j.error||'';load()}
|
|
112
121
|
async function load(){const r=await fetch('/api/board',{headers:{'x-crew-token':TOKEN}});const m=await r.json();if(m.error){document.getElementById('msg').textContent=m.error;return}
|
|
113
122
|
document.getElementById('time').textContent='updated '+new Date(m.generated_at).toLocaleTimeString();
|
|
123
|
+
const d=m.digest,c=d.counts,k=document.getElementById('kpis');k.replaceChildren();
|
|
124
|
+
for(const[n,l,cls]of[[c.ready,'ready',''],[c.claimed,'in progress',''],[c.human,'need a human',c.human?'bad':''],[c.stale,'stale leases',c.stale?'warn':''],[c.prs,'PRs waiting',''],[c.questions,'open questions',''],[c.idle,'idle members',c.idle?'warn':'']])k.append(e('div',{class:'kpi '+cls},e('b',{},String(n)),e('span',{},l)));
|
|
125
|
+
const al=document.getElementById('alerts');al.replaceChildren();const busy=d.sections.filter(s=>s.key!=='prs'&&s.lines.length);
|
|
126
|
+
for(const s of busy){const ul=e('ul');for(const l of s.lines)ul.append(e('li',{},l));al.append(e('div',{class:'card'},e('h2',{},s.title),ul))}
|
|
127
|
+
if(!busy.length)al.append(e('div',{class:'card ok'},'All clear: nobody waits for a human, no stale leases, everyone is busy.'));
|
|
114
128
|
const cols=document.getElementById('cols');cols.replaceChildren();
|
|
115
129
|
for(const c of COLS){const s=e('section',{},e('h2',{},c+' ('+m.columns[c].length+')'));
|
|
116
130
|
for(const t of m.columns[c]){const who=[['','unassigned'],...m.members.map(x=>[x,x])];
|