@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gredescerber/agent-crew",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Vendor-neutral crew of coding agents (Claude Code, Codex, ...) working as one team through GitHub Issues and PRs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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' });
@@ -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: ghJson(['issue', 'list', '--state', 'open', '--label', QUESTION_LABEL, '--limit', '100', '--json', 'number,title,body,labels,assignees,createdAt'], { cwd: root, allowFail: true }) ?? [],
23
- team: existsSync(p) ? parsePrd(readFileSync(p, 'utf8')).team : null,
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
- return { columns, untracked: other.length, prs: queue, members: [...new Set(members)].sort((a, b) => a.localeCompare(b)), generated_at: new Date(now).toISOString() };
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])];