@nanocollective/roster 0.1.0-alpha.11 → 0.1.0-alpha.13

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": "@nanocollective/roster",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.13",
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": {
@@ -117,3 +117,7 @@
117
117
  .verdict{background:var(--accent-soft);border:1px solid transparent;border-radius:var(--r);
118
118
  padding:9px 12px;font-size:12.5px}
119
119
  .verdict p{margin:0}
120
+
121
+ .plansteps{margin:6px 0 12px;padding-left:20px;font-size:13.5px;line-height:1.7}
122
+ .planfiles{margin:0 0 12px}
123
+ .planfiles summary{cursor:pointer;color:var(--ink-dim);font-size:12.5px}
@@ -10,6 +10,8 @@ import { el, toClipboard } from "../dom.js";
10
10
  const LEVEL_ORDER = { fail: 0, warn: 1, ok: 2 };
11
11
 
12
12
  /**
13
+ * @param opts.skip finding ids the caller already shows as a step of its own, so they are not
14
+ * reported twice. With `opts.hideWhenClean`, a list left empty draws nothing at all.
13
15
  * @param opts.shell when Health draws this, the section it lives in: `{actions, say}`. The
14
16
  * setup screen passes nothing and gets the layout it has always had. Its own screen is a
15
17
  * wizard, where a chunky button and a paragraph under it are right; on Health it is one
@@ -32,9 +34,14 @@ export async function checklist(host, opts = {}) {
32
34
  const findings = (report.findings ?? []).slice().sort(
33
35
  (a, b) => (LEVEL_ORDER[a.level] ?? 3) - (LEVEL_ORDER[b.level] ?? 3),
34
36
  );
35
- const bad = findings.filter((f) => f.level !== "ok");
37
+ const skip = new Set(opts.skip ?? []);
38
+ const bad = findings.filter((f) => f.level !== "ok" && !skip.has(f.id));
36
39
 
37
40
  host.replaceChildren();
41
+ if (!bad.length && opts.hideWhenClean) {
42
+ opts.onEmpty?.();
43
+ return;
44
+ }
38
45
  opts.shell?.say(bad.length ? String(bad.length) : "clean");
39
46
  host.append(
40
47
  el("p", {
@@ -83,7 +90,7 @@ export async function checklist(host, opts = {}) {
83
90
  const again = el("button", { className: btnClass, textContent: "Check again" });
84
91
  again.onclick = () => checklist(host, opts);
85
92
 
86
- const feet = opts.shell?.actions ?? host;
93
+ const feet = opts.shell?.actions ?? host.appendChild(el("div", { className: "row" }));
87
94
  if (opts.shell) feet.replaceChildren();
88
95
  feet.append(copy, again, note);
89
96
  if (bad.length) {
@@ -92,8 +99,7 @@ export async function checklist(host, opts = {}) {
92
99
  className: noteClass,
93
100
  style: "flex-basis:100%",
94
101
  textContent:
95
- "Paste it into Cursor, Claude Code or anything else that can edit files here, then " +
96
- "check again. The ids above should be gone.",
102
+ "Paste them into your coding agent, then check again.",
97
103
  }),
98
104
  );
99
105
  }
@@ -8,7 +8,7 @@ import { el } from "../dom.js";
8
8
 
9
9
  export function credentialPanel() {
10
10
  const box = el("div", { className: "manual" });
11
- box.append(el("b", { textContent: "Your agent's credential, once" }));
11
+ box.append(el("b", { textContent: "Agent credential" }));
12
12
  const body = el("div");
13
13
  box.append(body);
14
14
  body.append(el("p", { className: "sub", textContent: "Looking at where it would go…" }));
@@ -28,8 +28,7 @@ function draw(body, c) {
28
28
  el("p", {
29
29
  className: "sub",
30
30
  textContent:
31
- "Nothing reads it until somebody is hired, so this box opens once there is a staff " +
32
- "member. It is asked for once, not once per hire.",
31
+ "Available after your first hire. You only add it once.",
33
32
  }),
34
33
  );
35
34
  return;
@@ -40,8 +39,7 @@ function draw(body, c) {
40
39
  el("p", {
41
40
  className: "sub",
42
41
  textContent:
43
- c.name + " is already an org secret, shared with " + c.orgSecret.visibility + " repos. " +
44
- "Each hire adds its brain to it. Paste a new one only to replace it.",
42
+ c.name + " is already set. Paste a new one only to replace it.",
45
43
  }),
46
44
  );
47
45
  }
@@ -53,7 +51,7 @@ function draw(body, c) {
53
51
  (c.plan.mode === "org"
54
52
  ? "Stored as one org secret, " + c.name + ", shared with " + c.brains.join(", ") + ". "
55
53
  : "Stored as " + c.name + " on each of " + c.brains.join(", ") + ", because " + c.plan.reason + ". ") +
56
- "It goes to gh on standard input and is never written to disk.",
54
+ "It's never written to disk.",
57
55
  }),
58
56
  );
59
57
 
@@ -80,8 +78,8 @@ function draw(body, c) {
80
78
  el("p", {
81
79
  textContent:
82
80
  r.mode === "org"
83
- ? "Stored once for the org, shared with " + r.repos.length + " brains."
84
- : "Stored on " + r.repos.join(", ") + ".",
81
+ ? "Saved."
82
+ : "Saved on " + r.repos.join(", ") + ".",
85
83
  }),
86
84
  );
87
85
  } catch (err) {
@@ -18,9 +18,7 @@ export function repoPicker(opts) {
18
18
  el("b", { textContent: "Which repos the staff work in" }),
19
19
  el("p", {
20
20
  textContent:
21
- "Repos marked as products are the ones staff contribute to without owning. A new hire " +
22
- "picks them up automatically; somebody already hired keeps the works_in list in their " +
23
- "own staff.yaml.",
21
+ "Add the repos your staff should work on. New hires get access to them automatically.",
24
22
  }),
25
23
  );
26
24
 
@@ -30,6 +30,12 @@ let S = null; // /api/setup/status, refreshed after anything that changes the wo
30
30
  /* Where the doctor checklist is drawn on this screen, so a save can re-run it. Left stale, it
31
31
  went on saying business.stub after business.md had been saved, until "Check again". */
32
32
  let healthHost = null;
33
+ let healthStep = null;
34
+
35
+ /* What the "What is left" cards already ask for. Doctor reporting them as well, right after a
36
+ successful create, read as the create having failed. */
37
+ const COVERED = ["business", "business.stub", "priorities", "priorities.stub", "actions-access", "secrets", "agent"];
38
+ const HEALTH = { skip: COVERED, hideWhenClean: true };
33
39
 
34
40
  /* Each "what is left" step that is a file, by id, so a save can mark it done in place rather
35
41
  than repainting the page and throwing away the diff somebody is still reading. */
@@ -66,8 +72,7 @@ export async function viewGettingStarted(main) {
66
72
  next.append(
67
73
  el("p", {
68
74
  textContent:
69
- "Nothing runs until somebody is hired. Hiring creates their repo, their workflows and " +
70
- "their pinned issue; the GitHub App and the charter follow on their card.",
75
+ "Nothing runs until you hire someone. You'll set up their GitHub App and charter from their card.",
71
76
  }),
72
77
  el("div", { className: "row" }, [hire]),
73
78
  );
@@ -76,11 +81,12 @@ export async function viewGettingStarted(main) {
76
81
 
77
82
  main.append(afterCreate(App.data?.staff?.length ? 1 : 2));
78
83
 
79
- const health = step(App.data?.staff?.length ? 2 : 3, "What doctor still says", false);
84
+ const health = step(App.data?.staff?.length ? 2 : 3, "Other problems", false);
80
85
  healthHost = el("div", { style: "margin-top:12px" });
81
86
  health.append(healthHost);
82
87
  main.append(health);
83
- checklist(healthHost, {});
88
+ healthStep = health;
89
+ checklist(healthHost, { ...HEALTH, onEmpty: () => health.remove() });
84
90
  }
85
91
 
86
92
  /** Whether the sidebar offers Getting started: while anything is left, or while it is open. */
@@ -94,7 +100,7 @@ export function paintSetupNav() {
94
100
  again rather than updated by hand. The sidebar's list comes from /api/org, which is the one
95
101
  the portal already reads. */
96
102
  async function recheck() {
97
- if (healthHost) checklist(healthHost, {});
103
+ if (healthHost) checklist(healthHost, { ...HEALTH, onEmpty: () => healthStep?.remove() });
98
104
  try {
99
105
  S = await getSetup();
100
106
  for (const [id, mark] of fileSteps) mark(!(S.tenant.left ?? []).includes(id));
@@ -120,9 +126,7 @@ function render(main) {
120
126
  el("p", {
121
127
  className: "sub",
122
128
  textContent:
123
- "Sign in with gh, choose the organisation and the agent, then what is left: a setting " +
124
- "roster tries for you, the agent's credential, the repos your staff work in, and the two " +
125
- "files only you can write. Nothing is created until you have read the plan.",
129
+ "Choose an organisation and a coding agent. Nothing is created until you press Create it.",
126
130
  }),
127
131
  );
128
132
 
@@ -148,8 +152,7 @@ function stepGh() {
148
152
  el("p", {
149
153
  className: "sub",
150
154
  textContent:
151
- "roster does everything through your own gh, so it holds no token of its own. Run this " +
152
- "in a terminal, then reload:",
155
+ "Run this in a terminal, then reload:",
153
156
  }),
154
157
  el("pre", { className: "cmd", textContent: "gh auth login" }),
155
158
  );
@@ -170,13 +173,9 @@ function stepOrg(main) {
170
173
  el("p", {
171
174
  className: "sub",
172
175
  textContent:
173
- "Reload the page to open the portal. Anything below that is still undone stays under " +
174
- "Getting started in the sidebar until it is.",
176
+ "Finish the steps below. They stay under Getting started in the sidebar until they're done.",
175
177
  }),
176
178
  );
177
- healthHost = el("div", { style: "margin-top:12px" });
178
- card.append(healthHost);
179
- checklist(healthHost, {});
180
179
  return card;
181
180
  }
182
181
 
@@ -206,8 +205,7 @@ function stepOrg(main) {
206
205
  orgWrap.append(
207
206
  el("small", {
208
207
  textContent:
209
- "Repos are created here. GitHub has no API for creating an organisation, so make one on " +
210
- "github.com first if you need to.",
208
+ "Don't have one? Create it on github.com first.",
211
209
  }),
212
210
  );
213
211
  form.append(orgWrap);
@@ -250,8 +248,8 @@ function stepOrg(main) {
250
248
  });
251
249
  other.input.addEventListener("change", look);
252
250
 
253
- const name = field("name", "What the business is called", "", "Shown to the agents. Defaults to the org.");
254
- const human = field("human", "Your GitHub login", S.gh.login ?? "", "The person the agents answer to.");
251
+ const name = field("name", "What the business is called", "", "Defaults to the organisation name.");
252
+ const human = field("human", "Your GitHub login", S.gh.login ?? "", "The person the staff report to.");
255
253
 
256
254
  card.append(form);
257
255
  const agentHead = el("h3", { textContent: "Which coding agent runs a session" });
@@ -272,8 +270,7 @@ function stepOrg(main) {
272
270
  const agentNote = el("p", {
273
271
  className: "sub",
274
272
  textContent:
275
- "Anything else works too: a runner is an install command, a run command and the name of " +
276
- "the secret holding its credential. Write those three into org.yaml afterwards.",
273
+ "Using a different agent? Pick any of these, then set it in org.yaml afterwards.",
277
274
  });
278
275
  card.append(agentNote);
279
276
 
@@ -349,26 +346,20 @@ function stepOrg(main) {
349
346
  out.replaceChildren(el("p", { className: "sub", textContent: "Working…" }));
350
347
  try {
351
348
  const result = await planTenant(p);
352
- out.replaceChildren();
353
- out.append(
354
- el("p", {
355
- textContent:
356
- `${result.files.length} files in ${result.dir}, and one private repo, ${p.org}/roster-ops, ` +
357
- "with its Actions access set so every repo in the org can call its workflow. If GitHub " +
358
- "refuses that, you get the reason and the page to click.",
359
- }),
360
- );
361
- const list = el("div", { className: "filelist" });
362
- for (const f of result.files) list.append(el("code", { textContent: "+ " + f }));
363
- out.append(list);
364
- out.append(
365
- el("p", {
366
- className: "sub",
367
- textContent:
368
- "org/business.md arrives as questions, not prose. Answering it is the next step and " +
369
- "it is the one that decides whether any of this is worth running.",
370
- }),
371
- );
349
+ const steps = el("ol", { className: "plansteps" }, [
350
+ el("li", { textContent: `Create a private repo, ${p.org}/roster-ops, to hold the org's shared settings.` }),
351
+ el("li", { textContent: `Put a copy of it in ${result.dir}.` }),
352
+ el("li", { textContent: `Let the other repos in ${p.org} run its workflow.` }),
353
+ ]);
354
+ const files = el("details", { className: "planfiles" }, [
355
+ el("summary", { textContent: `The ${result.files.length} files it writes` }),
356
+ el("div", { className: "filelist" }, result.files.map((f) => el("code", { textContent: f }))),
357
+ ]);
358
+ const next = el("p", {
359
+ className: "sub",
360
+ textContent: "After that: describe the business, set this month's priorities, and hire your first staff member.",
361
+ });
362
+ out.replaceChildren(el("p", { textContent: "Create it will:" }), steps, files, next);
372
363
  go.hidden = false;
373
364
  } catch (err) {
374
365
  out.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) }));
@@ -401,13 +392,6 @@ function stepOrg(main) {
401
392
  function afterCreate(n) {
402
393
  fileSteps.clear();
403
394
  const card = step(n, "What is left", false);
404
- card.append(
405
- el("p", {
406
- textContent:
407
- "The first is a setting roster tries for you. The last two are the files that decide " +
408
- "whether any of this is worth running, and only you can write them.",
409
- }),
410
- );
411
395
  card.append(accessStep());
412
396
  card.append(credentialPanel());
413
397
 
@@ -417,9 +401,8 @@ function afterCreate(n) {
417
401
 
418
402
  const business = fileStep(
419
403
  "business",
420
- "Answer org/business.md",
421
- "It ships as questions. It is composed into the top of every prompt, every run, and an " +
422
- "agent that cannot answer them writes work that is plausible and generic.",
404
+ "Describe the business",
405
+ "Answer the questions in org/business.md. Every staff member reads it before they start work.",
423
406
  );
424
407
  // The copy-a-prompt loop, which is the whole answer to "how do I write this file".
425
408
  business.append(paste({ kind: "discover", title: "Write it with your own AI", onSaved: recheck }));
@@ -427,9 +410,8 @@ function afterCreate(n) {
427
410
 
428
411
  const priorities = fileStep(
429
412
  "priorities",
430
- "Rank org/priorities.md",
431
- "What matters this month, at most three things in order, and what is out of scope. Every " +
432
- "daily run reads it; without it each staff member picks its own direction from its charter.",
413
+ "Set this month's priorities",
414
+ "Up to three things, in order, plus what's out of scope. Saved as org/priorities.md.",
433
415
  );
434
416
  priorities.append(prioritiesEditor());
435
417
  card.append(priorities);
@@ -500,7 +482,7 @@ function prioritiesEditor() {
500
482
  el("p", {
501
483
  className: r.pushed ? "sub" : "err",
502
484
  textContent: r.pushed
503
- ? "Committed and pushed. Every daily run reads it from the next one."
485
+ ? "Saved and pushed."
504
486
  : "Committed, but the push failed: " + (r.note ?? ""),
505
487
  }),
506
488
  );
@@ -529,7 +511,7 @@ function opsName() {
529
511
  */
530
512
  function accessStep() {
531
513
  const box = el("div", { className: "manual" });
532
- box.append(el("b", { textContent: "Let every repo in the org call the ops repo's workflow" }));
514
+ box.append(el("b", { textContent: "Let staff repos run the shared workflow" }));
533
515
  const out = el("div");
534
516
  box.append(out);
535
517
  const say = (text, cls) => el("p", { className: cls ?? "sub", textContent: text });
@@ -539,8 +521,7 @@ function accessStep() {
539
521
  out.replaceChildren(
540
522
  say(
541
523
  (why ? why + " " : "") +
542
- "Settings → Actions → General → Access on roster-ops. Skip it and every workflow fails " +
543
- "with “workflow not found”.",
524
+ "Needed so staff repos can run the shared workflow.",
544
525
  ),
545
526
  el("div", { className: "row" }, [go]),
546
527
  );
@@ -549,7 +530,7 @@ function accessStep() {
549
530
  try {
550
531
  const r = await setAccess();
551
532
  if (r.ok) {
552
- out.replaceChildren(say("Set. Every repo in the org can call roster-ops."));
533
+ out.replaceChildren(say("Done."));
553
534
  return;
554
535
  }
555
536
  out.replaceChildren(
@@ -567,7 +548,7 @@ function accessStep() {
567
548
  out.replaceChildren(say("Checking…"));
568
549
  getAccess()
569
550
  .then((r) => {
570
- if (r.ok) out.replaceChildren(say("Done. Every repo in the org can call roster-ops."));
551
+ if (r.ok) out.replaceChildren(say("Done."));
571
552
  else offer(r.level ? "It is set to “" + r.level + "”." : "");
572
553
  })
573
554
  .catch(() => offer(""));