@nanocollective/roster 0.1.0-alpha.29 → 0.1.0-alpha.30

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/dist/cli.js CHANGED
@@ -37,6 +37,8 @@ function httpStatus(stderr) {
37
37
  return m ? Number(m[1]) : void 0;
38
38
  }
39
39
  function tidy(stderr) {
40
+ if (/rate limit/i.test(stderr))
41
+ return "GitHub's API limit for this hour is used up. It resets within the hour.";
40
42
  const msg = /"message"\s*:\s*"([^"]+)"/.exec(stderr);
41
43
  if (msg) return msg[1];
42
44
  const first = stderr.split("\n").map((l) => l.trim()).filter(Boolean)[0] ?? "unknown error";
@@ -4255,6 +4257,7 @@ function buildExport(ws, org, parseYaml, opts = {}) {
4255
4257
  statusIssue: manifest.status_issue,
4256
4258
  schedule: entry.schedule ?? manifest.schedule,
4257
4259
  mention: manifest.mention,
4260
+ icon: manifest.icon ? String(manifest.icon) : void 0,
4258
4261
  bots: [manifest.bot, manifest.public_bot].filter(Boolean).map(botName),
4259
4262
  soloBots: [],
4260
4263
  sharedBots: [],
@@ -5645,6 +5648,9 @@ async function fetchInbox(repos) {
5645
5648
  }
5646
5649
  })
5647
5650
  );
5651
+ const reasons = errors.map((e) => e.slice(e.indexOf(": ") + 2));
5652
+ if (errors.length > 1 && reasons.every((r) => r === reasons[0]))
5653
+ errors.splice(0, errors.length, reasons[0]);
5648
5654
  items.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
5649
5655
  return { items, errors };
5650
5656
  }
@@ -5802,6 +5808,8 @@ function rollup(state) {
5802
5808
  return "pending";
5803
5809
  }
5804
5810
  function short4(e) {
5811
+ const stderr = e?.stderr;
5812
+ if (stderr) return tidy(String(stderr));
5805
5813
  const msg = e instanceof Error ? e.message : String(e);
5806
5814
  const line = msg.split("\n").find((l) => l.trim() && !/^\s*$/.test(l)) ?? msg;
5807
5815
  return line.length > 200 ? line.slice(0, 199) + "\u2026" : line;
package/docs/export.md CHANGED
@@ -35,6 +35,7 @@ that reads it gets the same view without reimplementing the memory grammar.
35
35
  | `statusIssue` | pinned issue number |
36
36
  | `schedule` | cron |
37
37
  | `mention` | what wakes them |
38
+ | `icon` | the icon set by `icon:` in staff.yaml, if any |
38
39
  | `bots[]` | every App login, `[bot]` suffix stripped |
39
40
  | `soloBots[]` | identities unique to this staff member |
40
41
  | `sharedBots[]` | identities shared with others |
@@ -59,6 +59,7 @@ labels:
59
59
  | `handle` | yes | Must match `org.yaml`. The composer looks them up by the `org.yaml` one, so a mismatch composes the wrong brain. |
60
60
  | `name` | yes | Role name in prose. |
61
61
  | `mention` | yes | What wakes them, as in `@cto`. The caller's condition tests for this string. |
62
+ | `icon` | no | The icon the portal shows for them: `code`, `megaphone`, `life-buoy`, `compass`, `brush`, `bug`, `server`, `book-open`, `bar-chart`, `users` or `person`. Unset, it is picked from the role. |
62
63
  | `brain` | yes | `owner/name` of this repo. Without it nothing can check secrets, labels or runs. |
63
64
  | `status_issue` | yes in practice | Number of the pinned status issue. The prompts reference it, so a run cannot compose without it. `roster hire --apply` writes it. |
64
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanocollective/roster",
3
- "version": "0.1.0-alpha.29",
3
+ "version": "0.1.0-alpha.30",
4
4
  "description": "An agent-run org, powered by GitHub. Scaffold AI staff members whose brain is a repo.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -157,3 +157,15 @@ main{padding:36px 44px 96px;max-width:1220px;overflow-x:hidden}
157
157
  font:600 14px var(--sans);cursor:pointer}
158
158
  .hirecard:hover{color:var(--ink);border-color:var(--accent)}
159
159
  .hirecard::before{content:"+";font-size:22px;line-height:1}
160
+
161
+ /* The inbox toolbar: one height, one line. */
162
+ .inboxbar{align-items:center}
163
+ .inboxbar > *{height:34px;box-sizing:border-box}
164
+ .inboxbar .modes{margin:0;padding:3px;align-items:stretch}
165
+ .inboxbar .mode{display:flex;align-items:center}
166
+ .inboxbar .iconbtn{width:34px;display:grid;place-items:center}
167
+ .inboxbar .ghbtn{display:flex;align-items:center}
168
+ /* Staff and issue icons in the sidebar. */
169
+ .nav.inboxlink{display:flex;align-items:center;gap:8px}
170
+ .nav.inboxlink .ic,.staffrow .ic{--ic:14px;color:var(--ink-faint);flex:none}
171
+ .nav.inboxlink[aria-current="true"] .ic{color:var(--accent)}
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { getOrg } from "./api.js";
4
4
  import { $, el, store } from "./dom.js";
5
- import { icon, iconHTML } from "./icons.js";
5
+ import { icon, iconHTML, staffIcon } from "./icons.js";
6
6
  import { installLightbox } from "./lightbox.js";
7
7
  import { setPeople } from "./md.js";
8
8
  import {
@@ -268,6 +268,7 @@ function paintSidebar() {
268
268
  const dot = el("span", { className: "readydot", hidden: true });
269
269
  head.append(
270
270
  icon("chevron", "caret"),
271
+ icon(staffIcon(s), "ic"),
271
272
  el("span", { className: "t", textContent: s.name }),
272
273
  dot,
273
274
  el("span", { className: "hh", textContent: s.handle }),
@@ -322,20 +323,22 @@ function paintInboxLinks() {
322
323
  const sub = $("#inboxsub");
323
324
  if (!sub) return;
324
325
  sub.replaceChildren();
326
+ // Inbox itself is everything. Under it: each staff member, then the product repos' issues.
325
327
  const links = [
326
- ["", "", "All"],
327
- ["mine", "", "On " + humanLabel()],
328
- ["decision", "", "Decisions"],
329
- ...S.data.staff.map((s) => ["", s.handle, s.name]),
328
+ ...S.data.staff.map((s) => [s.handle, s.name, staffIcon(s)]),
329
+ ["@issues", "Issues", "issue-open"],
330
330
  ];
331
- for (const [filter, who, label] of links) {
332
- const b = el("button", { className: "nav subnav inboxlink", textContent: label });
333
- b.dataset.filter = filter;
331
+ for (const [who, label, glyph] of links) {
332
+ const b = el("button", { className: "nav subnav inboxlink" }, [
333
+ icon(glyph, "ic"),
334
+ el("span", { className: "t", textContent: label }),
335
+ ]);
334
336
  b.dataset.who = who;
335
337
  b.onclick = () => {
336
338
  S.view = "inbox";
337
- S.inboxFilter = filter;
338
- S.inboxStaff = who;
339
+ // A second click on the one you are on goes back to everything.
340
+ S.inboxStaff = S.inboxStaff === who ? "" : who;
341
+ S.inboxFilter = "";
339
342
  S.inboxOpen = null;
340
343
  render();
341
344
  };
@@ -347,10 +350,7 @@ function markSidebar() {
347
350
  const sub = $("#inboxsub");
348
351
  if (sub) sub.hidden = S.view !== "inbox";
349
352
  for (const b of document.querySelectorAll(".inboxlink")) {
350
- const on =
351
- S.view === "inbox" &&
352
- b.dataset.filter === (S.inboxFilter || "") &&
353
- b.dataset.who === (S.inboxStaff || "");
353
+ const on = S.view === "inbox" && b.dataset.who === (S.inboxStaff || "");
354
354
  b.setAttribute("aria-current", String(on));
355
355
  }
356
356
  for (const b of document.querySelectorAll(".nav")) {
@@ -13,6 +13,26 @@
13
13
  */
14
14
 
15
15
  const PATHS = {
16
+ "code":
17
+ "<polyline points=\"16 18 22 12 16 6\" /> <polyline points=\"8 6 2 12 8 18\" />",
18
+ "megaphone":
19
+ "<path d=\"m3 11 18-5v12L3 14v-3z\" /> <path d=\"M11.6 16.8a3 3 0 1 1-5.8-1.6\" />",
20
+ "life-buoy":
21
+ "<circle cx=\"12\" cy=\"12\" r=\"10\" /> <path d=\"m4.93 4.93 4.24 4.24\" /> <path d=\"m14.83 9.17 4.24-4.24\" /> <path d=\"m14.83 14.83 4.24 4.24\" /> <path d=\"m9.17 14.83-4.24 4.24\" /> <circle cx=\"12\" cy=\"12\" r=\"4\" />",
22
+ "compass":
23
+ "<circle cx=\"12\" cy=\"12\" r=\"10\" /> <polygon points=\"16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76\" />",
24
+ "brush":
25
+ "<path d=\"m9.06 11.9 8.07-8.06a2.85 2.85 0 1 1 4.03 4.03l-8.06 8.08\" /> <path d=\"M7.07 14.94c-1.66 0-3 1.35-3 3.02 0 1.33-2.5 1.52-2 2.02 1.08 1.1 2.49 2.02 4 2.02 2.2 0 4-1.8 4-4.04a3.01 3.01 0 0 0-3-3.02z\" />",
26
+ "bug":
27
+ "<path d=\"m8 2 1.88 1.88\" /> <path d=\"M14.12 3.88 16 2\" /> <path d=\"M9 7.13v-1a3.003 3.003 0 1 1 6 0v1\" /> <path d=\"M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6\" /> <path d=\"M12 20v-9\" /> <path d=\"M6.53 9C4.6 8.8 3 7.1 3 5\" /> <path d=\"M6 13H2\" /> <path d=\"M3 21c0-2.1 1.7-3.9 3.8-4\" /> <path d=\"M20.97 5c0 2.1-1.6 3.8-3.5 4\" /> <path d=\"M22 13h-4\" /> <path d=\"M17.2 17c2.1.1 3.8 1.9 3.8 4\" />",
28
+ "server":
29
+ "<rect width=\"20\" height=\"8\" x=\"2\" y=\"2\" rx=\"2\" ry=\"2\" /> <rect width=\"20\" height=\"8\" x=\"2\" y=\"14\" rx=\"2\" ry=\"2\" /> <line x1=\"6\" x2=\"6.01\" y1=\"6\" y2=\"6\" /> <line x1=\"6\" x2=\"6.01\" y1=\"18\" y2=\"18\" />",
30
+ "book-open":
31
+ "<path d=\"M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z\" /> <path d=\"M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z\" />",
32
+ "bar-chart":
33
+ "<line x1=\"12\" x2=\"12\" y1=\"20\" y2=\"10\" /> <line x1=\"18\" x2=\"18\" y1=\"20\" y2=\"4\" /> <line x1=\"6\" x2=\"6\" y1=\"20\" y2=\"16\" />",
34
+ "users":
35
+ "<path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\" /> <circle cx=\"9\" cy=\"7\" r=\"4\" /> <path d=\"M22 21v-2a4 4 0 0 0-3-3.87\" /> <path d=\"M16 3.13a4 4 0 0 1 0 7.75\" />",
16
36
  "check":
17
37
  "<path d=\"M20 6 9 17l-5-5\" />",
18
38
  "chevron":
@@ -96,6 +116,23 @@ export function icon(name, cls = "") {
96
116
  return span.firstChild ?? span;
97
117
  }
98
118
 
119
+ /* A staff member's icon: `icon:` in their staff.yaml, or one picked from their role. */
120
+ const ROLE_ICONS = {
121
+ cto: "code",
122
+ cmo: "megaphone",
123
+ support: "life-buoy",
124
+ pm: "compass",
125
+ designer: "brush",
126
+ qa: "bug",
127
+ devops: "server",
128
+ writer: "book-open",
129
+ analyst: "bar-chart",
130
+ community: "users",
131
+ };
132
+ export function staffIcon(s) {
133
+ return s?.icon && hasIcon(s.icon) ? s.icon : (ROLE_ICONS[s?.handle] ?? "person");
134
+ }
135
+
99
136
  export function hasIcon(name) {
100
137
  return Object.hasOwn(PATHS, name);
101
138
  }
@@ -150,7 +150,7 @@ function inboxScreen(m, opts) {
150
150
  };
151
151
  controls.push(newBtn);
152
152
  }
153
- m.append(el("div", { className: "row", style: "margin-bottom:16px" }, controls));
153
+ m.append(el("div", { className: "row inboxbar", style: "margin-bottom:16px" }, controls));
154
154
 
155
155
  const split = el("div", { className: "split" });
156
156
  const list = el("div", { className: "tree" });
@@ -232,12 +232,15 @@ function inboxScreen(m, opts) {
232
232
 
233
233
  // The sidebar's filters are the inbox's. Pending work has no control for them, so it ignores them.
234
234
  const staffPick = opts.prs ? "" : S.inboxStaff;
235
- const whoseStaff = staffPick ? S.data.staff.find((s) => s.handle === staffPick) : null;
235
+ // "Issues" is the product repos: every repo a staff member works in.
236
+ const products = new Set(S.data.staff.flatMap((s) => s.worksIn ?? []));
237
+ const issuesOnly = staffPick === "@issues";
238
+ const whoseStaff = staffPick && !issuesOnly ? S.data.staff.find((s) => s.handle === staffPick) : null;
236
239
  /* The two screens partition the org rather than overlap on it. Pending work is every pull
237
240
  request; the inbox is everything else. An inbox that also listed the PRs said the same
238
241
  thing twice and made the badge beside it a number you could not act on. */
239
242
  const mine = S.inbox.items
240
- .filter((i) => belongsTo(i, whoseStaff))
243
+ .filter((i) => (issuesOnly ? products.has(i.repo) : belongsTo(i, whoseStaff)))
241
244
  .filter((i) => (i.kind === "pr") === !!opts.prs);
242
245
  const isOpen = (i) => i.state === "OPEN";
243
246
  const scoped = mine.filter(